Search code examples
assemblyx86kernelbootloaderbios

Why does my conversion from LBA to CHS not work?


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.


Solution

  • The error in lba_to_chs

    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
    

    The errors in disk_init_lba

    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.