BIOS Always Fails to Perform Disk Operations

2

I am currently writing a bootloader designed to load a program longer than the bootsector will allow. However, every time I run the program (I test it in both Virtualbox and QEMU), the disk read fails, and the disk reset also fails.

The bootloader is designed to load the sector immediately after it (this is to be a FAT16 volume, so I have made this a reserved sector in the disk description), and run the program immediately after. However, the disk read always fails (CF gets set to 1), and the disk reset does the same. This happens in both Virtualbox and QEMU.

This is my complete code for both the bootsector and second sector:

BITS 16

jmp strict short main               ; Jump to main bootloader
nop                                 ; Pad out remaining bytes until boot descriptor

; Disk descriptor

OEM_name            db "HOUSE   "   ; Disk label
bytes_sector        dw 0x0200       ; Bytes per sector
sectors_cluster     db 0x01         ; Sectors per cluster
sectors_reserved    dw 0x0002       ; Number of sectors reserved (in this case 1 for MARBLE, the rest for R)
fats                db 0x02         ; Number of file allocation tables
max_root_entries    dw 0x0200       ; Max number of root entries
sectors             dw 0x1040       ; Number of sectors
medium_type         db 0xF0         ; Type of medium (removable or fixed?)
sectors_fat         dw 0x0010       ; Sectors per file allocation table
sectors_track       dw 0x0020       ; Sectors per track
heads               dw 0x0002       ; Number of heads
hidden_sectors      dd 0x00000000   ; Number of sectors before partition
total_sectors       dd 0x00000000   ; Number of sectors in medium (zero because 2B != 0)
drive_number        db 0x00         ; Drive number (for BIOS int 0x13)
drive_signature     db 0x00         ; NOT USED
ext_signature       db 0x29         ; Extended boot signature
volume_serial       dd 0x00000000   ; Volume's serial number
volume_label        db "HOUSEHOUSE "; Volume label
fs_type             db "FAT16   "   ; Filesystem type

main:
    mov sp, 0x1000                  ; 4K of stack

    mov ax, word [sectors_reserved] ; Read all reserved sectors
    sub al, 0x01                    ; Except this one
    mov ah, 0x02                    ; Read disk sectors
    mov bx, r_buffer                ; Read contents into our buffer
    mov ch, 0x00                    ; Cylinder 0
    mov cl, 0x02                    ; Sector 2
    mov dh, 0x00                    ; Head 0
    jmp .read                       ; Read the disk

.reset:
    pusha                           ; Push register states to stack
    call lps
    mov ah, 0x00                    ; Reset disk
    int 0x13                        ; BIOS disk interrupt
    jnc .read                       ; If successsul, read again
    call lps
    mov ah, 0x00                    ; Otherwise, prepare to reboot
    int 0x19                        ; Reboot

.read:
    call lps
    int 0x13                        ; BIOS disk interrupt
    jc .reset                       ; If failed, reset disk
    jmp r_buffer                    ; Otherwise, jump to R

lps:                                ; Debug message
    pusha
    mov ah, 0x0E
    mov al, 0x52
    mov bh, 0x00
    int 0x10
    mov ah, 0x00
    int 0x16
    popa
    ret

    times 510-($-$$) db 0x00        ; Pad remainder of boot sector with zeros
    sig             dw 0xAA55       ; Boot signature

r_buffer:                           ; Space in memory for loading R

r_start:                            ; Beginning of second sector
    mov ax, 0x07C0
    add ax, 0x0220
    mov ss, ax
    mov ax, 0x07C0
    mov ds, ax

    mov si, success                 ; Successful
    call print_str                  ; Print!
    hlt                             ; Halt here

print_str:                          ; Prints string pointed to by REGISTER SI to cursor location (si=str)
    pusha                           ; Push register states to stack
    mov ah, 0x0E                    ; Print in teletype mode
    mov bh, 0x00                    ; Page 0

.repeat:
    lodsb                           ; Load next character from si
    cmp al, 0x00                    ; Is this a null character?
    je .ret                         ; If it is, return to caller
    int 0x10                        ; Otherwise, BIOS interrupt
    jmp .repeat                     ; Do the same thing

.ret:
    popa                            ; Restore register states
    ret                             ; Return to caller

.data:
    success         db "!", 0x00    ; Success!

    times 1024-($-$$) db 0x00       ; Pad remainder of reserved sectors with zeros

As I said, the code in the second sector is supposed to be run, but this doesn't happen as the disk reset fails.

assembly
nasm
x86-16
bootloader
bios
asked on Stack Overflow Aug 2, 2019 by House • edited Aug 2, 2019 by Michael Petch

1 Answer

2

@ecm picked up on most things I saw. Before accessing any data you need to set up the segment registers and ensure you have an ORG (origin point) that is proper for the values you load in the segment registers (especially DS). By default when there is no ORG directive (or equivalent) the default is 0x0000.

In real-mode every logical address is made up of 2 components - a segment and an offset. The physical address that is computed by the CPU is based on the formula (segment*16)+offset. Think of ORG as the starting offset. If you use a segment of 0x0000 then you need an offset of 0x7c00. (0x0000*16)+0x7c00=0x7c00. 0x7c00 is where the bootloader starts. Since you are writing sectors to memory above 0x7e00 it would be simpler to set the stack to 0x0000:0x7c00 to grow down from just under the bootloader towards the beginning of memory.

Since you are using string instructions like LODSB you should ensure the direction flag (DF) is cleared with the CLD instruction so that string operations advance forward in memory. You can't rely on DF being clear when your bootloader starts executing.

When reading the disk Int 13h/AH=2 clobbers AX. If there is an error you will need to reload AH with 2 and AL with the number of sectors to read. If you rework the code you can use SI to store the number of sectors to read temporarily. Usually you don't have to check if a disk reset fails, just redo the read operation again. Try the operation a few times and then go into a failure state / reboot. I modified your code to put a retry count in DI. Each time a reset is done the retry count is reduced by 1. If the retry count >= 0 then the read is attempted. If the retry count is <= 0 then it reboots.

When jumping to the code at memory address 0x7e00 where the reserved sectors are read to you can take the opportunity to ensure CS is set to 0x0000 as well by using a FAR JMP.

If you set up the segment to 0x0000 in the boot sector you can reuse them in the second stage. If you intend to enter protected mode from your bootloader I do not recommend using segment values other than 0x0000 so that logical addresses and physical addresses in the first 64KiB are the same. This is very useful when loading a GDT and making the jump into protected mode.

When using HLT it is a good idea to make sure interrupts are off (use CLI) otherwise the HLT will exit on the next interrupt and the processor will continue executing what happens to be in memory after that. On real hardware it is possible that a Non-Maskable Interrupt (NMI) can occur even with interrupts off so it is a good idea to put the HLT in a loop.

With these changes in mind, this code should work:

BITS 16

org 0x7c00

jmp strict short main               ; Jump to main bootloader
nop                                 ; Pad out remaining bytes until boot descriptor

; Disk descriptor

OEM_name            db "HOUSE   "   ; Disk label
bytes_sector        dw 0x0200       ; Bytes per sector
sectors_cluster     db 0x01         ; Sectors per cluster
sectors_reserved    dw 0x0002       ; Number of sectors reserved (in this case 
                                    ; 1 for MARBLE, the rest for R)
fats                db 0x02         ; Number of file allocation tables
max_root_entries    dw 0x0200       ; Max number of root entries
sectors             dw 0x1040       ; Number of sectors
medium_type         db 0xF0         ; Type of medium (removable or fixed?)
sectors_fat         dw 0x0010       ; Sectors per file allocation table
sectors_track       dw 0x0020       ; Sectors per track
heads               dw 0x0002       ; Number of heads
hidden_sectors      dd 0x00000000   ; Number of sectors before partition
total_sectors       dd 0x00000000   ; Number of sectors in medium (zero because 2B != 0)
drive_number        db 0x00         ; Drive number (for BIOS int 0x13)
drive_signature     db 0x00         ; NOT USED
ext_signature       db 0x29         ; Extended boot signature
volume_serial       dd 0x00000000   ; Volume's serial number
volume_label        db "HOUSEHOUSE "; Volume label
fs_type             db "FAT16   "   ; Filesystem type

main:
    xor ax, ax                      ; AX = 0
    mov ds, ax                      ; DS = ES = 0
    mov es, ax
    mov ss, ax                      ; SS:SP = 0x0000:0x7c00 (grows down below bootloader)
    mov sp, 0x7c00
    cld                             ; Set forward direction for string instructions

    mov si, word [sectors_reserved] ; Read all reserved sectors
    dec si                          ; Except this one. SI = sectors to read
    mov di, 3                       ; retry count of 3 and then give up

    mov bx, r_buffer                ; Read contents into our buffer
    mov ch, 0x00                    ; Cylinder 0
    mov cl, 0x02                    ; Sector 2
    mov dh, 0x00                    ; Head 0
    jmp .read                       ; Read the disk

.reset:
    call lps
    dec di                          ; Reduce retry count
    jge .read                       ; If retry >= 0 then try again

                                    ; Otherwise retry count exceeded - reboot
    mov ah, 0x00
    int 0x19                        ; Reboot

.read:
    mov ax, si                      ; Transfer sector read count to AX
    mov ah, 0x02                    ; Read disk sectors
    call lps
    int 0x13                        ; BIOS disk interrupt
    jc  .reset                      ; If failed, reset disk
    jmp 0x0000:r_start              ; Otherwise, jump to r_start. Set CS=0

lps:                                ; Debug message
    pusha
    mov ah, 0x0E
    mov al, 0x52
    mov bh, 0x00
    int 0x10
    mov ah, 0x00
    int 0x16
    popa
    ret

    times 510-($-$$) db 0x00        ; Pad remainder of boot sector with zeros
    sig             dw 0xAA55       ; Boot signature

r_buffer:                           ; Space in memory for loading R

r_start:                            ; Beginning of second sector
    mov si, success                 ; Successful
    call print_str                  ; Print!

    cli
.endloop:
    hlt                             ; Halt here
    jmp .endloop

print_str:                          ; Prints string pointed to by REGISTER SI 
                                    ;     to cursor location (si=str)
    pusha                           ; Push register states to stack
    mov ah, 0x0E                    ; Print in teletype mode
    mov bh, 0x00                    ; Page 0

.repeat:
    lodsb                           ; Load next character from si
    cmp al, 0x00                    ; Is this a null character?
    je .ret                         ; If it is, return to caller
    int 0x10                        ; Otherwise, BIOS interrupt
    jmp .repeat                     ; Do the same thing

.ret:
    popa                            ; Restore register states
    ret                             ; Return to caller

.data:
    success         db "!", 0x00    ; Success!

    times 1024-($-$$) db 0x00       ; Pad remainder of reserved sectors with zeros

Observations and Recommendations

  • For bootloader development I highly recommend using BOCHS for initial testing. The built in debugger is real-mode aware and understands real-mode segmentation whereas QEMU/GDB does not. Learning to use debugging tools is a valuable skill.
answered on Stack Overflow Aug 2, 2019 by Michael Petch • edited Aug 2, 2019 by Michael Petch

User contributions licensed under CC BY-SA 3.0