Search code examples
assemblyx86bootloadergnu-assemblerbios

How to deal with errors while reading disk in x86 assembly?


I am trying to make my own bootloader in GAS assembly. So far, I am able to print to the screen using BIOS interrupts. I tried to read the disk into memory, but the output in the emulator is:

Booting...
(PANIC) Disk error
Press any key to reboot..._

This is my code:

.code16
.text
.org 0x0

.global main

main:
    jmp start                       # jump to beginning of code
    nop

bpb:
    iOEM:          .ascii "Canary"      # OEM String
    iSectSize:     .word  0x200         # bytes per sector
    iClustSize:    .byte  1             # sectors per cluster
    iResSect:      .word  1             # #of reserved sectors
    iFatCnt:       .byte  2             # #of FAT copies
    iRootSize:     .word  224           # size of root directory
    iTotalSect:    .word  2880          # total # of sectors if over 32 MB
    iMedia:        .byte  0xf0          # media Descriptor
    iFatSize:      .word  9             # size of each FAT
    iTrackSect:    .word  9             # sectors per track
    iHeadCnt:      .word  2             # number of read-write heads
    iHiddenSect:   .int   0             # number of hidden sectors
    iSect32:       .int   0             # # sectors for over 32 MB
    iBootDrive:    .byte  0             # holds drive that the boot sector came from
    iReserved:     .byte  0             # reserved, empty
    iBootSign:     .byte  0x29          # extended boot sector signature
    iVolID:        .ascii "seri"        # disk serial
    acVolumeLabel: .ascii "VOLUME A"    # volume label
    acFSType:      .ascii "FAT16"       # file system type

.func print
print:
    lodsb           # load byte from si into al, increment si
    cmp $0, %al     # test if character is 0 (end)
    je print_done   # jump to end if 0.

    mov $0x0e, %ah  # set teletype output
    mov $9, %bx     # set bh (page no.) to 0, and bl (attribute) to white (9)
    int $0x10       # int 10h

    jmp print       # repeat for next character.

print_done:
    ret
.endfunc

.func reboot
reboot:
    mov $rebootmsg, %si  # load address of reboot message into si
    call print           # print the string
    mov $0x00, %ah
    mov $0x00, %al
    int $0x16            # wait for a key press
    .byte 0xea           # machine language to jump to ffff:0000 (reboot)
    .word 0x0000
    .word 0xffff
.endfunc

.func readSector
readSector:
    mov $0x00, %cx  # counter = 0

read:
    push %ax        # store logical block in stack
    push %cx        # store counter in stack
    push %bx        # store data buffer offset in stack

    # Cylinder = (LBA / SectorsPerTrack) / NumHeads
    # Sector   = (LBA mod SectorsPerTrack) + 1
    # Head     = (LBA / SectorsPerTrack) mod NumHeads

    mov iTrackSect, %bx    # get sectors per track
    mov $0x00, %dx

    # Divide (dx:ax/bx to ax,dx)
    # Quotient (ax) =  LBA / SectorsPerTrack
    # Remainder (dx) = LBA mod SectorsPerTrack
    div %bx

    inc %dx         # increment remainder since it is a sector
    mov %dl, %cl    # store result in cl to use for int 13h

    mov iHeadCnt, %bx  # get number of heads
    mov $0x00, %dx

    # Divide (dx:ax/bx to ax,dx)
    # Quotient (ax) = Cylinder
    # Remainder (dx) = Head
    div %bx

    mov %al, %ch    # ch = cylinder
    mov %dl, %dh    # dh = head

    mov $0x02, %ah          # subfunction 2
    mov $0x01, %al          # no. of sectors to read
    mov iBootDrive, %dl     # drive number
    pop %bx                 # restore data buffer offset
    int $0x13
    jc readFailure          # retry if carry flag is set (error)

    pop %cx
    pop %ax
    ret

# On error, retry 4 times before jumping to bootFailure
readFailure:
    pop %cx         # get counter from stack
    inc %cx
    cmp $4, %cx     # check if we completed 4 tries
    je bootFailure  # jump to bootFailure if even after 4 tries we get an error

    # Reset disk system
    mov $0x00, %ah
    mov $0x00, %al
    int $0x13

    # Retry
    pop %ax
    jmp read
.endfunc

start:
    # Setup segments:
    cli
    mov %dl, iBootDrive  # save what drive we booted from (should be 0x0)
    mov %cs, %ax         # cs = 0x0, since that's where boot sector is (0x07c00)
    mov %ax, %ds         # cs = cs = 0x0
    mov %ax, %es         # cs = cs = 0x0
    mov %ax, %ss         # cs = cs = 0x0
    mov $0x7c00, %sp     # Stack grows down from offset 0x7c00 toward 0x0000.
    sti

    # Clear the screen
    mov $0x00, %ah
    mov $0x03, %al # Set video mode (80x25 text mode, 16 colors)
    int $0x10

    # Reset disk system
    # Jump to bootFailure on error
    mov iBootDrive, %dl   # drive to reset
    mov $0x00, %ah
    mov $0x00, %al
    int $0x13
    jc bootFailure        # display error message if carry set (error)

    # Display message if successful
    mov $msg, %si
    call print

    call readSector

    # Reboot
    call reboot

bootFailure:
    mov $diskerror, %si
    call print
    call reboot

# Program Data
msg:        .asciz "Booting...\r\n"
diskerror:  .asciz "(PANIC) Disk error\r\n"
rebootmsg:  .asciz "Press any key to reboot..."

.fill (510-(.-main)), 1, 0  # Pad with nulls up to 510 bytes (excl. boot magic)
.word 0xaa55                # magic word for BIOS

What am I doing wrong? Also, if there is a better more efficient way to write this code, please tell.


Solution

  • Your bpb is in the wrong place and has the wrong length!

        jmp start                       # jump to beginning of code
        nop
    bpb:
        iOEM:          .ascii "Canary"      # OEM String 
        ...
        acVolumeLabel: .ascii "VOLUME A"    # volume label
        acFSType:      .ascii "FAT16"       # file system type
    

    Because the start label is more than 127 bytes away from this initial jmp, the assembler will encode the jump using 3 bytes. But then the additional nop will make the bpb start at offset 4, where legally it has to start at offset 3. Either you drop the nop or you bring the start closer by.

    The BS_OEMName field must be 8 bytes long. Your "Canary" is too short.

    The BS_VolLab field must be 11 bytes long. Your "VOLUME A" is too short.

    The BS_FilSysType field must be 8 bytes long. Your "FAT16" is too short.


    Setting up segment registers is wrong.

    start:    # Setup segments:
        cli
        mov %dl, iBootDrive  # save what drive we booted from (should be 0x0)
        mov %cs, %ax         # cs = 0x0, since that's where boot sector is (0x07c00)
        mov %ax, %ds         # cs = cs = 0x0
        mov %ax, %es         # cs = cs = 0x0
        mov %ax, %ss         # cs = cs = 0x0
        mov $0x7c00, %sp     # Stack grows down from offset 0x7c00 toward 0x0000.
        sti
    

    There's no guarantee whatsoever that the %cs code segment register will be 0 when your bootsector program starts, yet you copy its contents to the other segment registers. Moreover, with an .org 0x0 the correct value to load in the segment registers would have to be 0x07C0, not zero!
    And of course you should defer saving the bootdrive code to after you have loaded %ds correctly.

    My suggestion would be:

    .org 0x7C00
    
    ...
    
    start:
      cli
      xor  %ax, %ax
      mov  %ax, %ds
      mov  %ax, %es
      mov  %ax, %ss
      mov  $0x7C00, %sp
      mov  %dl, iBootDrive
      sti
    

    Why it has to fail reading from disk

    call print
    
    call readSector
    

    You don't specify the logical block that you want to read from!
    The print routine certainly uses %ax leaving a non-sensical value in it, and you never setup the desired LBA in %ax before calling readSector. That's never going to work. Additionally you also forget to specify the data buffer offset in %bx.

    Once you'll have amended all of the above, you could try to load the second sector of the disk (CHS = 0,0,2) so as to verify that reading is fine:

    mov  $0x00, %dh       # dh = head
    mov  iBootDrive, %dl  # drive number
    mov  $0x0002, %cx     # ch = cylinder, cl = sector
    mov  $0x7E00, %bx     # data buffer offset
    mov  $0x0201, %ax     # ah = subfunction, al = no. of sectors to read
    int  $0x13
    jc   readFailure
    

    Good luck!

    Also, if there is a better more efficient way to write this code, please tell.

    I didn't verify the code within the readSector routine too much. I'm kind of assuming that it copied anyway. Point being, if you're interested in optimizing the code, I suggest that you, after corrections, post a working version of it on the Code Review forum.