I am writing a stage 1 bootloader in assembly with which I am attempting to load the FAT12 filesystem into memory so that I can load my stage 2 bootloader. I have managed to load the FATs into memory, however I am struggling to load the root directory into memory.
I am currently using this for reference and have produced the following:
.load_root:
;es is 0x7c0
xor dx, dx ; blank dx for division
mov si, fat_loaded ; inform user that FAT is loaded
call print
mov al, [FATcount] ; calculate how many sectors into the disk must be loaded
mul word [SectorsPerFAT]
add al, [ReservedSectors]
div byte [SectorsPerTrack]
mov ch, ah ; Store quotient in ch for cylinder number
mov cl, al ; Store remainder in cl for sector number
xor dx, dx
xor ax, ax
mov al, ch ; get back to "absolute" sector number
mul byte [SectorsPerTrack]
add al, cl
mul word [BytesPerSector]
mov bx,ax ; Memory offset to load to data into memory after BOTH FATs (should be 0x2600, physical address should be 0xA200)
xor dx, dx ; blank dx for division
mov ax, 32
mul word [MaxDirEntries]
div word [BytesPerSector] ; number of sectors root directory takes up (should be 14)
xor dh, dh ; head 0
mov dl, [boot_device] ; boot device
mov ah, 0x02 ; select read mode
int 13h
cmp ah, 0
je .load_OS
mov si, error_text
call print
jmp $
However, if I inspect the memory at 0xA200
with gdb, I just see 0s. My root directory does contain a file -- I have put a file called OS.BIN in the root directory to test with.
Using info registers
in gdb after the read operation gives the following output:
eax 0xe 14
ecx 0x101 257
edx 0x0 0
ebx 0x2600 9728
esp 0x76d0 0x76d0
ebp 0x0 0x0
esi 0x16d 365
edi 0x0 0
eip 0x7cdd 0x7cdd
eflags 0x246 [ PF ZF IF ]
cs 0x0 0
ss 0x53 83
ds 0x7c0 1984
es 0x7c0 1984
fs 0x0 0
gs 0x0 0
The status of the operation is 0, the number of sectors read is 14, and es:bx
points to 0xA200, but x/32b 0xa200
shows 32 0s, when I would expecting to see the data for OS.BIN.
EDIT
I did info registers
before the interrupt and the output is the following:
eax 0x20e 526
ecx 0x101 257
edx 0x0 0
ebx 0x2600 9728
esp 0x76d0 0x76d0
ebp 0x0 0x0
esi 0x161 353
edi 0x0 0
eip 0x7cc8 0x7cc8
eflags 0x246 [ PF ZF IF ]
cs 0x0 0
ss 0x53 83
ds 0x7c0 1984
es 0x7c0 1984
fs 0x0 0
gs 0x0 0
Which is the same as after, except the function request number has been replaced with the status code.
Where am I going wrong? Am I reading from the wrong CHS address? Or some other simple mistake? And how can I correct this?
I am using fat_imgen
to make my disk image. Command for creating the disk image is fat_imgen -c -f floppy.flp -F -s bootloader.bin
and command for adding OS.BIN
to the image is fat_imgen -m -f floppy.flp -i OS.BIN
I have a BIOS Parameter Block (BPB) that represents a 1.44MB floppy using FAT12:
jmp short loader
times 9 db 0
BytesPerSector: dw 512
SectorsPerCluster: db 1
ReservedSectors: dw 1
FATcount: db 2
MaxDirEntries: dw 224
TotalSectors: dw 2880
db 0
SectorsPerFAT: dw 9
SectorsPerTrack: dw 18
NumberOfHeads: dw 2
dd 0
dd 0
dw 0
BootSignature: db 0x29
VolumeID: dd 77
VolumeLabel: db "Bum'dOS ",0
FSType: db "FAT12 "
I have another function that appears to work that loads the FAT12 table to memory address 0x7c0:0x0200 (physical address 0x07e00):
;;;Start loading File Allocation Table (FAT)
.load_fat:
mov ax, 0x07c0 ; address from start of programs
mov es, ax
mov ah, 0x02 ; set to read
mov al, [SectorsPerFAT] ; how many sectors to load
xor ch, ch ; cylinder 0
mov cl, [ReservedSectors] ; Load FAT1
add cl, byte 1
xor dh, dh ; head 0
mov bx, 0x0200 ; read data to 512B after start of code
int 13h
cmp ah, 0
je .load_root
mov si, error_text
call print
hlt
I ended up scrapping loading the root directory after loading the FATs. In the end, I modified my .load_fat routine to load both FATs and the root directory at the same time (essentially reading 32 sectors after the boot sector, but in a way that still allows me to easily modify the disk geometry).
The code for this is below:
.load_fat:
mov ax, 0x07c0 ; address from start of programs
mov es, ax
mov al, [SectorsPerFAT] ; how many sectors to load
mul byte [FATcount] ; load both FATs
mov dx, ax
push dx
xor dx, dx ; blank dx for division
mov ax, 32
mul word [MaxDirEntries]
div word [BytesPerSector] ; number of sectors for root directory
pop dx
add ax, dx ; add root directory length and FATs length -- load all three at once
xor dh,dh
mov dl, [boot_device]
xor ch, ch ; cylinder 0
mov cl, [ReservedSectors] ; Load from after boot sector
add cl, byte 1
xor dh, dh ; head 0
mov bx, 0x0200 ; read data to 512B after start of code
mov ah, 0x02 ; set to read
int 13h
cmp ah, 0
je .load_root
mov si, error_text
call print
hlt
Though not the way I intended to solve the problem, it does the job and I can move on from this to continue development.
EDIT
I think I worked out where the old code was going wrong, anyway. I was incrementing the cylinder after sector 18, when I should have been incrementing the head. It's CHS, not HCS, for a reason!
Analysis of Problem
The issue with your code is that you aren't reading from the point on the disk that you are expecting. Although your disk read is successful it has loaded the wrong sectors into memory.
If we look at Ralph Brown's Interrupt List for Int 13h/AH=2 we'll see that the inputs look like this:
DISK - READ SECTOR(S) INTO MEMORY
AH = 02h
AL = number of sectors to read (must be nonzero)
CH = low eight bits of cylinder number
CL = sector number 1-63 (bits 0-5)
high two bits of cylinder (bits 6-7, hard disk only)
DH = head number
DL = drive number (bit 7 set for hard disk)
ES:BX -> data buffer
If we review your registers before you do int 13h
in .load_root
we see these registers with the following contents:
eax 0x20e
ecx 0x101
edx 0x0
ebx 0x2600
es 0x7c0
So ES:BX is 0x7c0:0x2600 which is physical address 0xA200. That is correct. AH (0x02) is disk read and the number of sectors to read in AL is 14 (0x0e). This seems reasonable. The issue arises in ECX and EDX. If we review your code it appears you are attempting to find the sector (Logical Block Address) on disk where the root directory starts:
mov al, [FATcount] ; calculate how many sectors into the disk must be loaded
mul word [SectorsPerFAT]
add al, [ReservedSectors]
In your BIOS Parameter Block you have SectorsPerFat
= 9, ReservedSectors
= 1, and FATCount
= 2. If we review a FAT12 design document that shows this configuration it would look like:
Your calculation is correct. 2*9+1 = 19. The first 19 Logical Blocks run from LBA 0 to LBA 18. LBA 19 is where your root directory starts. We need to convert this to Cylinders/Heads/Sectors (CHS). Logical Block Address to CHS calculation:
CHS tuples can be mapped to LBA address with the following formula:
LBA = (C × HPC + H) × SPT + (S - 1)
where C, H and S are the cylinder number, the head number, and the sector number
LBA is the logical block address
HPC is the maximum number of heads per cylinder (reported by
disk drive, typically 16 for 28-bit LBA)
SPT is the maximum number of sectors per track (reported by
disk drive, typically 63 for 28-bit LBA)
LBA addresses can be mapped to CHS tuples with the following formula
("mod" is the modulo operation, i.e. the remainder, and "÷" is
integer division, i.e. the quotient of the division where any
fractional part is discarded):
C = LBA ÷ (HPC × SPT)
H = (LBA ÷ SPT) mod HPC
S = (LBA mod SPT) + 1
In your code SPT = 18, HPC = 2. If we use an LBA of 19 we compute a CHS of C=0, H=1, S=2. If we look at the values you passed into the registers (CL, CH, DH) above we'd discover you used a CHS of C=1, H=0, S=1. This happens to be LBA 36, not 19. The issue is that your calculations are wrong. In particular .load_root
:
div byte [SectorsPerTrack]
mov ch, ah ; Store quotient in ch for cylinder number
mov cl, al ; Store remainder in cl for sector number
[snip]
xor dh, dh ; head 0
mov dl, [boot_device] ; boot device
mov ah, 0x02 ; select read mode
int 13h
Unfortunately this isn't a correct way of calculating CHS from an LBA. You have a similar issue with .load_fat
but you get lucky that you compute the right value. You are reading from the wrong sectors on the disk and that is causing data to be loaded at 0xA200 that you aren't expecting.
Translation of LBA to CHS
What you need is a proper LBA to CHS conversion routine. Since you will need such a function for different aspect of navigating FAT12 file structures it is best to create a function. We'll call it lba_to_chs
.
Before we write such code we should revisit the equation earlier:
C = LBA ÷ (HPC × SPT)
H = (LBA ÷ SPT) mod HPC
S = (LBA mod SPT) + 1
We could implement this as is, but if we rework the equation for cylinders we can reduce the amount of work we have to do. C = LBA ÷ (HPC × SPT)
can be rewritten as:
C = LBA ÷ (HPC × SPT)
C = LBA ÷ (SPT × HPC)
C = (LBA ÷ SPT) × (1 ÷ HPC)
C = (LBA ÷ SPT) ÷ HPC
If we now look at the revised formula we have:
C = (LBA ÷ SPT) ÷ HPC
H = (LBA ÷ SPT) mod HPC
S = (LBA mod SPT) + 1
Now we should notice that (LBA ÷ SPT)
is duplicated in two places. We only have to do that equation once. As well since x86 DIV instruction computes the remainder and quotient at the same time we also end up computing LBA mod SPT
for free when we do (LBA ÷ SPT)
. The code would follow this structure:
- Compute LBA DIV SPT . This yields:
(LBA ÷ SPT)
in the quotient
(LBA mod SPT)
in the remainder
- Take the remainder from step (1) and put in temporary register
- Add 1 to the temporary in step (2). That register now contains the sector as computed by
S = (LBA mod SPT) + 1
- Take quotient from step (1) and divide by HPC.
- Cylinder number will be the quotient
- Head will be the remainder.
We have reduced the equation down to a couple DIV instructions and an increment/add. We can simplify things more. If we assume we are using well known IBM Compatible Disk formats then we can also say that Sectors per Track (SPT), Heads(HPC), Cylinder, Head, and Sector will always be less than 256. When the maximum LBA on any well known floppy disk format is divided by SPT the result will always be less than 256. Knowing this allows us to avoid bit twiddling the top two bits of the cylinder and placing them in the top two bits of CL. We can also use DIV instructions that do 16-bit by 8-bit unsigned division.
Translation Code
If we take the pseudo code above we can create a rather small lba_to_chs
function that takes an LBA and converts it to CHS and only works for well known IBM compatible floppy disk formats.
; Function: lba_to_chs
; Description: Translate Logical block address to CHS (Cylinder, Head, Sector).
; Works ONLY for well known IBM PC compatible floppy disk formats.
;
; Resources: http://www.ctyme.com/intr/rb-0607.htm
; https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
; https://stackoverflow.com/q/45434899/3857942
; Sector = (LBA mod SPT) + 1
; Head = (LBA / SPT) mod HEADS
; Cylinder = (LBA / SPT) / HEADS
;
; Inputs: SI = LBA
; Outputs: DL = Boot Drive Number
; DH = Head
; CH = Cylinder
; CL = Sector
;
; Notes: Output registers match expectation of Int 13h/AH=2 inputs
;
lba_to_chs:
push ax ; Preserve AX
mov ax, si ; Copy 16-bit LBA to AX
div byte [SectorsPerTrack] ; 16-bit by 8-bit DIV : LBA / SPT
mov cl, ah ; CL = S = LBA mod SPT
inc cl ; CL = S = (LBA mod SPT) + 1
xor ah, ah ; Upper 8-bit of 16-bit value set to 0 for DIV
div byte [NumberOfHeads] ; 16-bit by 8-bit DIV : (LBA / SPT) / HEADS
mov ch, al ; CH = C = (LBA / SPT) / HEADS
mov dh, ah ; DH = H = (LBA / SPT) mod HEADS
mov dl, [boot_device] ; boot device, not necessary to set but convenient
pop ax ; Restore scratch register
ret
If you need a version that can handle all the valid disk geometries supported by FAT12 then the code would have to use 32-bit/16-bit DIV instructions and one has to deal with cylinders being 10 bits instead of 8. Sample code could look like:
; Function: lba_to_chs
; Description: Translate Logical block address to CHS (Cylinder, Head, Sector).
; Works for all valid FAT12 compatible disk geometries.
;
; Resources: http://www.ctyme.com/intr/rb-0607.htm
; https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
; https://stackoverflow.com/q/45434899/3857942
; Sector = (LBA mod SPT) + 1
; Head = (LBA / SPT) mod HEADS
; Cylinder = (LBA / SPT) / HEADS
;
; Inputs: SI = LBA
; Outputs: DL = Boot Drive Number
; DH = Head
; CH = Cylinder (lower 8 bits of 10-bit cylinder)
; CL = Sector/Cylinder
; Upper 2 bits of 10-bit Cylinders in upper 2 bits of CL
; Sector in lower 6 bits of CL
;
; Notes: Output registers match expectation of Int 13h/AH=2 inputs
;
lba_to_chs:
push ax ; Preserve AX
mov ax, si ; Copy LBA to AX
xor dx, dx ; Upper 16-bit of 32-bit value set to 0 for DIV
div word [SectorsPerTrack] ; 32-bit by 16-bit DIV : LBA / SPT
mov cl, dl ; CL = S = LBA mod SPT
inc cl ; CL = S = (LBA mod SPT) + 1
xor dx, dx ; Upper 16-bit of 32-bit value set to 0 for DIV
div word [NumberOfHeads] ; 32-bit by 16-bit DIV : (LBA / SPT) / HEADS
mov dh, dl ; DH = H = (LBA / SPT) mod HEADS
mov dl, [boot_device] ; boot device, not necessary to set but convenient
mov ch, al ; CH = C(lower 8 bits) = (LBA / SPT) / HEADS
shl ah, 6 ; Store upper 2 bits of 10-bit Cylinder into
or cl, ah ; upper 2 bits of Sector (CL)
pop ax ; Restore scratch registers
ret
You can use either of these lba_to_chs
functions the same way and integrate them into your .load_fat
and .load_root
code. Your code could look like:
;;;Start loading File Allocation Table (FAT)
.load_fat:
mov ax, 0x07c0 ; address from start of programs
mov es, ax
mov ah, 0x02 ; set to read
mov al, [SectorsPerFAT] ; how many sectors to load
mov si, [ReservedSectors] ; Load FAT1 into SI for input to lba_to_chs
call lba_to_chs ; Retrieve CHS parameters and boot drive for LBA
mov bx, 0x0200 ; read data to 512B after start of code
int 13h
cmp ah, 0
je .load_root
mov si, error_text
call print
hlt
;;;Start loading root directory
.load_root:
mov si, fat_loaded
call print
xor ax, ax
mov al, [FATcount]
mul word [SectorsPerFAT]
add ax, [ReservedSectors] ; Compute LBA of oot directory entries
mov si, ax ; Copy LBA to SI for later call to lba_to_chs
mul word [BytesPerSector]
mov bx,ax ; Load to after BOTH FATs in memory
xor dx, dx ; blank dx for division
mov ax, 32
mul word [MaxDirEntries]
div word [BytesPerSector] ; number of sectors to read
call lba_to_chs ; Retrieve CHS values and load boot drive
mov ah, 0x02
int 13h
cmp ah, 0
je .load_OS
mov si, error_text
call print
jmp $