x86 BIOS stage 1 boot code halting after loop from interrupt

78 Views Asked by At

I have some x86 BIOS boot code (16 bit real mode):

org 0x7C00                              ; tell compiler where we are located in memory
bits 16                                 ; tell compiler that we are in 16 bit mode

%define ENDL 0x0D, 0x0A

; FAT12 header
jmp short start                         ; jump past FAT12 header and to actual code
nop                                     ; no operation

BPB_OEM_ID: db 'FADOS0.0'               ; OEM Identifier
BPB_BPS:    dw 512                      ; Bytes per Sector
BPB_SPC:    db 1                        ; Sectors per Cluster
BPB_RS:     dw 1                        ; Reserved Sectors
BPB_FC:     db 2                        ; Number of FATs on the drive
BPB_RDE:    dw 0E0h                     ; Root Dir Entry Count
BPB_TSC:    dw 2880                     ; Total Sector Count: 2880 * 512 = 1.44MB
BPB_MDT:    db 0F0h                     ; Media Descriptor Type: F0 = 3.5" floppy disk
BPB_SPF:    dw 9                        ; Sectors Per FAT
BPB_SPT:    dw 18                       ; Sectors Per Track
BPB_HC:     dw 2                        ; Head Count
BPB_HS:     dd 0                        ; Hidden Sectors
BPB_LSC:    dd 0                        ; Large Sector Count
EBR_DN:     db 0                        ; Drive Number: 0x00 floppy, 0x80 HDD, almost never used
            db 0                        ; Windows NT Flags: not used / reserved
EBR_SIG:    db 29h                      ; Boot Signature: this tells if the EBR data is present
EBR_VID:    db 12h, 34h, 56h, 78h       ; Volume ID: Drive Serial Number, almost never used
EBR_VL:     db 'FADOS0.0dev'            ; Volume Label: Name of the Drive, it can be whatever
EBR_SID:    db 'FAT12   '               ; System Identifier: FAT type

; Code section                          
start:                                  
    ; setup data segments               
    mov ax, 0                           ; can't set ds/es directly
    mov ds, ax                          ; set data segment to 0
    mov es, ax                          ; set extra segment to 0

    ; setup stack               
    mov ss, ax                          ; set stack segment to 0
    mov sp, 0x7C00                      ; stack grows downwards from where it is loaded into memory
    
    ; ensure we are loaded into memory in the right location
    push es
    push word .after
    retf

.after:                                 ; Get disk info and write it to FAT12 header
    mov [EBR_DN], dl                    ; BIOS should set DL to drive number
    
    ; show loading message
    mov si, msg_loading                 ; move loading message into si
    call print                          ; print message

    ; read drive parameters instead of relying on data on formatted disk
    push es                             ; save extra segment pointer to stack
    mov ah, 08h                         ; move 08 hex into ah
    int 13h                             ; call interupt 13 hex
    jc floppy_error                     ; if carry flag is set, jump to floppy read error handler
    pop es                              ; restore extra segment pointer from stack

    and cl, 0x3F                        ; remove top 2 bits
    xor ch, ch                          ; set ch to 0
    mov [BPB_SPT], cx                   ; sector count

    inc dh                              ; add 1 to dh
    mov [BPB_HC], dh                    ; head count

    ; compute LBA of root directory = reserved + fats * sectors_per_fat
    ; note: this section can be hardcoded
    mov ax, [BPB_SPF]                   ; move Sectors Per FAT into ax
    mov bl, [BPB_FC]                    ; move FAT Count into bl
    xor bh, bh                          ; set bh to 0
    mul bx                              ; ax = (fats * sectors_per_fat)
    add ax, [BPB_RS]                    ; ax = LBA of root directory
    push ax                             ; save ax to stack

    ; compute size of root directory = (32 * number_of_entries) / bytes_per_sector
    mov ax, [BPB_RDE]                   ; move Root Dir Entries into ax
    shl ax, 5                           ; ax *= 32
    xor dx, dx                          ; dx = 0
    div word [BPB_BPS]                  ; number of sectors we need to read

    test dx, dx                         ; if dx != 0, add 1
    jz .root_dir_after                  ;
    inc ax                              ; division remainder != 0, add 1
                                        ; this means we have a sector only partially filled with entries
.root_dir_after:

    ; read root directory
    mov cl, al                          ; cl = number of sectors to read = size of root directory
    pop ax                              ; ax = LBA of root directory
    mov dl, [EBR_DN]                    ; dl = drive number (we saved it previously)
    mov bx, buffer                      ; es:bx = buffer
    call disk_read                      ; read from disk

    ; search for stage 2
    xor bx, bx                          ; set bx to 0
    mov di, buffer                      ; move buffer into di

.search_stage2:
    mov si, stage2_bin                  ; move stage2 filename into si
    mov cx, 11                          ; compare up to 11 characters
    push di                             ; save di to stack
    repe cmpsb                          ; repeat while equal; compare ds:si with es:di
    pop di                              ; restore di from stack
    je .found_stage2                    ; jump to .found_stage2 if equal

    add di, 32                          ; add 32 to di
    inc bx                              ; add 1 to bx
    cmp bx, [BPB_RDE]                   ; compare Root Dir Entries to bx
    jl .search_stage2                   ; loop this code block if previous operation returns less than

    ; stage 2 not found
    jmp stage2_not_found_error          ; jump to floppy error handler

.found_stage2:

    ; di should have the address to the entry
    mov ax, [di + 26]                   ; first logical cluster field (offset 26)
    mov [stage2_cluster], ax            ; move ax into stage2 cluster

    ; load FAT from disk into memory
    mov ax, [BPB_RS]                    ; move Reserved Sectors into ax
    mov bx, buffer                      ; move buffer into bx
    mov cl, [BPB_SPF]                   ; move Sectors Per FAT into cl
    mov dl, [EBR_DN]                    ; move Drive Number into dl
    call disk_read                      ; read from disk

    ; read stage 2 and process FAT chain
    mov bx, STAGE2_LOAD_SEGMENT         ; move stage2 segment into bx
    mov es, bx                          ; move bx to extra segment pointer
    mov bx, STAGE2_LOAD_OFFSET          ; move stage2 offset into bx

.load_stage2_loop:
    
    ; Read next cluster
    mov ax, [stage2_cluster]            ; move stage2_cluster into ax
    
    ; not nice :( hardcoded value
    add ax, 31                          ; first cluster = (stage2_cluster - 2) * sectors_per_cluster + start_sector
                                        ; start sector = reserved + fats + root directory size = 1 + 18 + 134 = 33
    mov cl, 1                           ; move 1 into cl
    mov dl, [EBR_DN]                    ; move the drive number into dl
    call disk_read                      ; read from the disk

    add bx, [BPB_BPS]                   ; move Bytes Per Sector into bx

    ; compute location of next cluster
    mov ax, [stage2_cluster]            ; move stage2_cluster into ax
    mov cx, 3                           ; move 3 into cx
    mul cx                              ; multiply cx by ax
    mov cx, 2                           ; move 2 into cx
    div cx                              ; ax = index of entry in FAT, dx = cluster mod 2

    mov si, buffer                      ; move buffer into si
    add si, ax                          ; add ax to si
    mov ax, [ds:si]                     ; read entry from FAT table at index ax

    or dx, dx                           ; check if ax is zero
    jz .even                            ; if ax is zero jump to .even

.odd:
    shr ax, 4                           ; shift ax to the right by 4 bits
    jmp .next_cluster_after             ; jump to .next_cluster_after

.even:
    and ax, 0x0FFF                      ; move 0x0FFF into ax

.next_cluster_after:
    cmp ax, 0x0FF8                      ; subtract ax from 0x0FF8: end of chain
    jae .read_finish                    ; jump is result was above or equal

    mov [stage2_cluster], ax            ; move the cluster stage2 is located in into ax
    jmp .load_stage2_loop               ; jump to the stage 2 loading loop

.read_finish:
    
    ; jump to stage 2
    mov dl, [EBR_DN]                    ; move boot drive number into dl

    mov ax, STAGE2_LOAD_SEGMENT         ; move the stage 2 location into ax: set segment registers
    mov ds, ax                          ; move data segment to the stage 2 location
    mov es, ax                          ; move extra segment to the stage 2 location

    ; jump to the 2nd stage bootloader
    jmp STAGE2_LOAD_SEGMENT:STAGE2_LOAD_OFFSET

    jmp wait_key_and_reboot             ; should never happen, here just incase

    cli                                 ; disable interrupts, this way CPU can't get out of halt state
    hlt                                 ; halt processor

; Error handlers

floppy_error:
    mov si, msg_read_fail               ; move string to print into si
    call print                          ; print error
    jmp wait_key_and_reboot             ; jump to wait_key_and_reboot

stage2_not_found_error:
    mov si, msg_stage2_err              ; move string to print into si
    call print                          ; print error
    jmp wait_key_and_reboot             ; jump to wait_key_and_reboot

wait_key_and_reboot:
    mov ah, 0                           ; move 0 into ah
    int 16h                             ; wait for keypress
    jmp 0FFFFh:0                        ; jump to beginning of BIOS, should reboot

.halt:
    cli                                 ; disable interrupts, this way CPU can't get out of halt state
    hlt                                 ; halt processer 

; print text to screen
; ds:si = pointer to string to print

print:
    push si                             ; save si to stack
    push ax                             ; save ax to stack
    push bx                             ; save bx to stack

.loop:
    lodsb                               ; loads next character in al
    or al, al                           ; verify if next character is null
    jz .done                            ; if next character is null then jump to .done

    mov ah, 0x0E                        ; call bios interrupt
    mov bh, 0                           ; set page number to 0
    int 0x10                            ; call interupt 0x10

    jmp .loop                           ; loop printing until all characters have been printed

.done:
    pop bx                              ; restore bx from stack
    pop ax                              ; restore ax from stack
    pop si                              ; restore si from stack
    ret                                 ; return from function

; convert an lba address to a chs address
; ax = LBA address
; returns:
; cx = [bits 0-5]: sector number
; ch = [bits 6-15]: cylinder
; dh = head

lba_to_chs:

    push ax                             ; save ax to stack
    push dx                             ; save ax to stack

    xor dx, dx                          ; dx = 0
    div word [BPB_SPT]                  ; ax = LBA / SectorsPerTrack
                                        ; dx = LBA % SectorsPerTrack

    inc dx                              ; dx = (LBA % SectorsPerTrack + 1) = sector
    mov cx, dx                          ; cx = sector

    xor dx, dx                          ; dx = 0
    div word [BPB_HC]                   ; ax = (LBA / SectorsPerTrack) / Heads = cylinder
                                        ; dx = (LBA / SectorsPerTrack) % Heads = head
    mov dh, dl                          ; dh = head
    mov ch, al                          ; ch = cylinder (lower 8 bits)
    shl ah, 6
    or cl, ah                           ; put upper 2 bits of cylinder in CL

    pop ax                              ; restore ax from stack
    mov dl, al                          ; restore dl from stack
    pop ax                              ; restore ax from stack
    ret                                 ; return from function

; read from disk
; ax = LBA address
; cl = number of sectors to read (up to 128)
; dl = drive number
; es:bx = memory address where to store read data

disk_read:

    push ax                             ; save ax to stack
    push bx                             ; save bx to stack
    push cx                             ; save cx to stack
    push dx                             ; save dx to stack
    push di                             ; save di to stack

    push cx                             ; temporarily save CL (number of sectors to read)
    call lba_to_chs                     ; compute CHS address
    pop ax                              ; al = number of sectors to read
    
    mov ah, 02h                         ; move 02 in hex into ah
    mov di, 3                           ; retry count = 5

.retry:
    pusha                               ; save all registers, we don't know what bios modifies
    stc                                 ; set carry flag, some BIOS chips don't set it
    int 13h                             ; call interupt 13h: carry flag cleared = success
    jnc .done                           ; jump to .done if carry not set

    ; read failed
    popa                                ; restore all general purpose registers from stack
    call disk_reset                     ; reset disk controller

    dec di                              ; subtract 1 from di
    test di, di                         ; set zero flag if di and di AND to 0
    jnz .retry                          ; if result is not zero then retry

.fail:
    jmp floppy_error                    ; jump to error handler if all attempts failed

.done:
    popa                                ; restore all general purpose registers from stack

    pop di                              ; restore di regitser from stack
    pop dx                              ; restore dx register from stack
    pop cx                              ; restore cx register from stack
    pop bx                              ; restore bx register from stack
    pop ax                              ; restore ax register from stack
    ret                                 ; return from function

; reset disk controller
; ah = drive number

disk_reset:
    pusha                               ; push all values in registers to stack
    mov ah, 0                           ; move 0 into ah
    stc                                 ; set carry flag
    int 13h                             ; call interupt 13 in hex
    jc floppy_error                     ; jump if carry is set
    popa                                ; pop all values from stack
    ret                                 ; return from function

; data section

msg_loading:            db 'LOADING', ENDL, 0
msg_read_fail:          db 'DISK READ FAIL', ENDL, 0
msg_stage2_err:         db 'STAGE2.BIN NOT FOUND', ENDL, 0
stage2_bin:             db 'STAGE2  BIN', ENDL, 0
stage2_cluster:         dw 0
STAGE2_LOAD_SEGMENT     equ 0x2000
STAGE2_LOAD_OFFSET      equ 0

times 510-($-$$) db 0                   ; fill remaning space with 0s
dw 0AA55h                               ; last 2 bytes of boot sector

buffer:                                 ; buffer space

It is freezing after calling BIOS interrupt 0x10 to print "LOADING" to the screen (which it does do). I have run it in bochs and it gets stuck in a loop in a function in a BIOS routine, after this loop runs for an indeterminable amount of time, the program halts, seemingly somewhere in BIOS. Can someone help me fix this issue? I can't seem to find any solutions to this issue on my own, so I have come here.

I have tried to remove the printing to screen but it still halts somewhere in BIOS, and nothing is printed so the error handlers most likely didn't kick in on detecting an error, so I am completely lost now.

1

There are 1 best solutions below

3
ecm On

I assembled your code and tried it out, it works as expected to load a small kernel. However, it will overflow the near pointer in bx if you try to load a file that's larger than 64 KiB. I don't know whether that is why you observed a hang though.

I installed your loader into a 1440 KiB diskette image using the DOS command instsect B: /s12=20240318.bin and booted into my debugger then ran boot fdb and your loader works.

Modifying your loader as follows allows me to use your loader to load a recent version of my bootable debugger, lDebug:

First, after mov sp, 0x7C00 add mov bp, sp. lDebug's FreeDOS style entrypoint expects a boot sector with BPB pointed to by ss:bp.

Second, after adding to bx (we assume that the bx overflow will result in a zero):

    add bx, [BPB_BPS]                   ; move Bytes Per Sector into bx
    jnz .no_es_adjust
    mov ax, es
    add ah, 1000h >> 8
    mov es, ax
.no_es_adjust:

(To load lDebug I used the instsect switch /f3=ldebug.com. The first two names detected by instsect are in your error messages until I get around to fixing my code to disallow embedded blanks. I didn't expect error messages in all caps so it misdetects the messages as filenames.)