I'm developing a bootloader for an x86 BIOS. In my first-stage bootloader (MBR), I need to read 2880 sectors (or more) from the disk, and then jump to the second-stage bootloader placed in the second sector of the disk. The second stage will then load a kernel file using FAT16, which I'll implement later.
The function works for LBA values lower than 65, which is sufficient for loading the second stage written in C.
I have defined BPB_SecPerTrk as 18 and BPB_NumHeads as 2
Here is my function code in assembly:
; convert LBA to CHS
; input: si = LBA
; output: ch = cylinder, dh = head, cl = sector
lba_to_chs:
push bx ; save bx
mov ax, si ; load LBA address into ax
; Calculate sectors (CL)
xor dx, dx ; clear dx
div word [BPB_SecPerTrk] ; ax = LBA / SPT, dx = LBA % SPT
mov cl, dl ; cl = (LBA % SPT) + 1 (sector)
inc cl ; increment cl by 1
; Calculate head (DH)
xor dx, dx ; clear dx
div word [BPB_NumHeads] ; ax = LBA / (SPT * NumHeads), dx = (LBA / SPT) % NumHeads
mov dh, dl ; dh = (LBA / SPT) % NumHeads (head)
; Calculate cylinder (CH)
mov ch, al ; ch = ax (cylinder number, lower 8 bits)
mov al, ah ; al = ah (upper 8 bits of cylinder number)
shl al, 6 ; shift upper 2 bits of cylinder to higher bits
or ch, al ; combine them with lower 8 bits of ch
pop bx ; restore bx
ret
and also here is function responsible for disk init.
disk_init_lba:
pusha
; check if lba extension is supperted
mov ah, 0x41 ; check extensions
mov bx, 0x55AA ; magic number
mov dl, 0x80 ; disk number
int 0x13 ; call BIOS
stc ; DEBUG: implicitly disable reading disk using int 0x13 extensions
jc .lba_ext_not_sup ; if carry flag is set, jump to error handler
jmp .read_lba_ext ; if not, jump to read disk using LBA
.read_lba_ext:
mov si, DAPACK ; load DAP address to si
mov ah, 0x42 ; extended read function
mov dl, 0x80 ; disk number
int 0x13 ; call BIOS
jc .fail ; if carry flag is set, jump to error handler
jmp .ok ; if not, jump to success handler
.lba_ext_not_sup:
call print_disk_lba_sup_fail ; print failure message
jmp .read_lba_via_chs ; jump to read disk using CHS
.read_lba_via_chs:
clc ; clear carry flag if for some reason it was set
xor si, si ; LBA = 0
xor di, di ; set di to 0
mov bx, START_STAGE1 ; buffer for sector
jmp .loop ; jump to loop
.loop:
inc si ; increment LBA
add bx, 0x200 ; next sector buffer
call lba_to_chs ; convert LBA to CHS
mov ah, 0x02 ; read disk BIOS function
mov al, 0x01 ; number of sectors to read
mov dl, 0x80 ; disk number 0
int 0x13 ; call BIOS
jc .retry ; if carry flag is set, jump to error handler
; FIXME: reading LBAs above 65
; TODO: read up to 1.44 MB (2879 sectors)
cmp si, 65 ; check if we read enough sectors to fill 1.44 MB
jle .loop ; if true read next sector
jmp .ok ; if not, jump to success handler
.retry:
inc di ; increment di
cmp di, 3 ; check if we tried 3 times
jne .loop ; if not, retry
jmp .fail ; if yes, jump to error handler
.fail:
call print_disk_read_fail ; print failure message
jmp .exit ; jump to exit
.ok:
call print_disk_read_ok ; print success message
jmp .exit ; jump to exit
.exit:
popa
ret
I think it is related to value overflow some where in the code.
As found by @ecm the 2 most significant bits for the cylinder number belong to the CL register (bits 6 and 7), so change or ch, al
into or cl, al
, or else consider using next shorter version of the conversion code:
; IN (si) OUT (cx,dh) MOD (ax,dl)
lba_to_chs:
mov ax, si ; LBA
xor dx, dx
div word [BPB_SecPerTrk]
mov cx, dx
inc cx ; Sector
cwd
div word [BPB_NumHeads]
mov dh, dl ; Head
shl ah, 6
xchg al, ah
or cx, ax ; Cylinder
ret
It is not unusual for disk operations to need being repeated. That's why you have that retry count of 3 in the DI register. What you did however is allow 3 retries for all the sectors together where you should actually be allowing a couple of retries per sector.
But it is even worse than that, you are not retrying on the same sector at all! The "increment LBA" and "next sector buffer" operations must not intervene while retrying.
clc ; clear carry flag if for some reason it was set xor si, si ; LBA = 0
No need for this clc
, the xor si, si
that follows clears the carry flag anyway.
disk_init_lba:
pusha
...
.read_lba_via_chs:
xor si, si ; LBA = 0
mov bx, START_STAGE1 ; buffer for sector
.NextSector:
inc si ; increment LBA
add bx, 0x200 ; next sector buffer
mov di, 3 ; Tries per sector
.NextTry:
call lba_to_chs ; convert LBA to CHS
mov ax, 0x0201 ; read 1 sector
mov dl, 0x80 ; disk number
int 0x13 ; call BIOS
jc .retry
; FIXME: reading LBAs above 65
; TODO: read up to 1.44 MB (2879 sectors)
cmp si, 65 ; check if we read enough sectors
jbe .NextSector
jmp .ok ; if not, jump to success handler
.retry:
dec di ; more tries?
jnz .NextTry ; yes
.fail:
call print_disk_read_fail ; print failure message
jmp .exit
.ok:
call print_disk_read_ok ; print success message
.exit:
popa
ret
; FIXME: reading LBAs above 65
Instead of modifying BX, keep it fixed and advance the ES segment register by 32 (512 / 16).
disk_init_lba:
pusha
push es
...
.read_lba_via_chs:
xor si, si ; LBA = 0
mov bx, START_STAGE1 ; buffer for sector
.NextSector:
inc si ; increment LBA
mov ax, es ; next sector buffer ES:BX
add ax, 32
mov es, ax
mov di, 3 ; Tries per sector
.NextTry:
call lba_to_chs ; convert LBA to CHS
mov ax, 0x0201 ; read 1 sector
mov dl, 0x80 ; disk number
int 0x13 ; call BIOS
jc .retry
cmp si, 65 ; check if we read enough sectors
jbe .NextSector
jmp .ok
.retry:
dec di ; more tries?
jnz .NextTry ; yes
.fail:
call print_disk_read_fail ; print failure message
jmp .exit
.ok:
call print_disk_read_ok ; print success message
.exit:
pop es
popa
ret
; TODO: read up to 1.44 MB (2879 sectors)
You can't hope to read that many bytes. Your present code is confined to using the conventional memory, so at most a little less than 655360 bytes would be feasible...
More than enough for your second stage I would say.