Klaus Maria
Klaus Maria

Reputation: 107

Custom Bootloader doesn't find Kernel

I've been working on a hobby bootloader and kernel, but the bootloader never finds the kernel no matter what. Here is the bootloader:

%define BUFFER_SEG 0x2000
%define BUFFER_OFF 0x0000
%define LOAD_SEG 0x1000
%define LOAD_OFF 0x0000

[bits 16]
[org 0x7c00]

jmp short start
nop

;DISK DESCRIPTION(BIOS PARAMETER BLOCK)
OEMLabel        db "BOOT    "
BytesPerSector      dw 512
SectorsPerCluster   db 1
ReservedForBoot     dw 1
NumberOfFats        db 2
RootDirEntries      dw 224      ; Number of entries in root dir
                    ; (224 * 32 = 7168 = 14 sectors to read)
LogicalSectors      dw 2880
MediumByte      db 0F0h
SectorsPerFat       dw 9
SectorsPerTrack     dw 18       ; Sectors per track (36/cylinder)
Sides           dw 2
HiddenSectors       dd 0
LargeSectors        dd 0
DriveNo         dw 0
Signature       db 0x29 
VolumeID        dd 00000000h
VolumeLabel     db "myOS       "
FileSystem      db "FAT12   "

;BOOTLOADER
start:
    xor ax, ax
    mov ds, ax
    cli
    mov ss, ax
    mov sp, 0x7c00
    cld
    clc
    sti
    mov [drive], dl

load_root:
    mov ax, 19
    call lba_to_hts
    mov ah, 2
    mov al, 14
    mov si, BUFFER_SEG
    mov es, si
    mov bx, BUFFER_OFF
    int 13h
    jc reset
    mov si, load_root_str
    call print

search_file:
    mov di, BUFFER_OFF
    mov cx, word [RootDirEntries]
    xor ax, ax
.loop_search:
    xchg cx, dx
    mov si, filename
    mov cx, 11
    rep cmpsb
    je file_found
    add ax, 32
    mov di, BUFFER_OFF
    add di, ax
    xchg dx, cx
    loop .loop_search
    jmp file_not_found

file_found:
    mov ax, word [es:di+15]
    mov [cluster], ax
    mov ax, 1
    call lba_to_hts
    mov di, BUFFER_OFF
    mov bx, di
    mov ah, 2
    mov al, 9

load_FAT:
    mov si, FAT_str
    call print
    int 13h
    jnc load_file
    call reset
    jnc load_FAT
    jmp disk_error

load_file:
    mov si, load_file_str
    call print
    mov ax, LOAD_SEG
    mov es, ax
    xor bx, bx
    mov ah, 2
    mov al, 1
.load_sector:
    mov ax, word [cluster]
    add ax, 31
    call lba_to_hts
    mov ax, LOAD_SEG
    mov es, ax
    mov bx, word [pointer]
    pop ax
    push ax
    ;stc
    int 13h
    jnc next_cluster
    call reset
    jmp .load_sector

next_cluster:
    mov ax, [cluster]
    xor dx, dx
    mov bx, 3
    mul bx
    mov bx, 2
    div bx
    mov si, BUFFER_OFF
    add si, ax
    mov ax, word [ds:si]
    or dx, dx
    jz .even
.odd:
    shr ax, 4
    jmp short finish_load
.even:
    and ax, 0FFFh

finish_load:
    mov word [cluster], ax
    cmp ax, 0FF8h
    jae .jump_to_file
    add word [pointer], 512
    jmp next_cluster
.jump_to_file:
    pop ax
    mov dl, byte [drive]
    jmp LOAD_SEG:LOAD_OFF

;SUBROUTINES
file_not_found:
    mov si, not_found_str
    call print
    jmp reboot
print:
    pusha
    mov ah, 0x0E
.next:
    lodsb
    cmp al,0
    je .done
    int 0x10
    jmp .next
.done:
    popa
    ret
lba_to_hts:
    push ax
    push bx
    mov bx, ax
    xor dx, dx
    div word [SectorsPerTrack]
    add dl, 1
    mov cl, dl
    mov ax, bx
    xor dx, dx
    div word [SectorsPerTrack]
    xor dx, dx
    div word [Sides]
    mov dh, dl
    mov ch, al
    pop ax
    pop bx
    mov dl, [drive]
    ret
reset:
    mov ah, 0
    int 13h             ;reset disk
    jc disk_error           ;if failed jump to search fail
    ret
disk_error:
    mov si, disk_error_str
    call print
reboot:
    mov si, reboot_pmpt
    call print
    mov ax, 0
    int 16h
    mov ax, 0
    int 19h

;DATA
load_root_str db 'Loading Root',13,10,0
disk_error_str db 'Disk Error!',13,10,0
reboot_pmpt db 'PRESS A KEY TO REBOOT',13,10,0
not_found_str db 'KERNEL NOT FOUND',13,10,0
FAT_str db 'Loading FAT',13,10,0
load_file_str db 'Loading KERNEL',13,10,0
drive dw 0
cluster dw 0
pointer dw 0
filename db 'KERNEL  BIN',0
;PADDING AND SIGNATURE
times (510-($-$$)) db 0x00
dw 0AA55h

Here is the Kernel:

[bits 16]               ;16-bit binary format

;VECTORS
os_vectors:
    jmp os_main

;KERNEL
os_main:
    cli             ;clear interrupts
    mov ss, ax          ;set stack segment and pointer
    mov sp, 0FFFFh
    sti             ;restore interrupts
    cld             ;set RAM direction(for strings)
    mov ax, 1000h           ;set registers to match kernel location
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov si, hello           ;print welcome
    call print_string
    hlt

;SUBROUTINES
print_string:
    mov ah, 0x0e

.next_char:
    lodsb
    cmp al,0
    je .done_print
    int 0x10
    jmp .next_char

.done_print:
    ret

;DATA
hello db 'Hello',0

;PADDING
times (512-($-$$)) db 0x00

I padded the rest of the sector out, since I heard some emulators don't read it correctly if it isn't one full sector-size. I use the commands

#! bin/bash
cd image
hdiutil create -fs MS-DOS -sectors 2880 floppy
cd ../system
nasm -f bin boot.asm -o boot.bin
nasm -f bin kernel.asm -o kernel.bin
cd ..
dd conv=notrunc if=system/boot.bin of=image/floppy.dmg
dev=`hdid -nomount image/floppy.dmg`
sudo mkdir tmp-loop
sudo cp system/kernel.bin tmp-loop/
sudo mount -t msdos ${dev} tmp-loop
diskutil umount tmp-loop
hdiutil detach ${dev}
sudo rm -rf tmp-loop
hdiutil convert image/floppy.dmg -format UDTO -o image/image.iso

to build and then emulate it in qemu. I am doing this on a Macbook Air. When I emulate my bootloader in qemu I always get the string telling me it can't find the kernel. I don't know why nor how to fix this.

Upvotes: 1

Views: 278

Answers (1)

Michael Petch
Michael Petch

Reputation: 47573

For the most part your code has the right idea. The primary problems in the bootloader are in load_file and next_cluster. There is also a bug in lba_to_hts. Your kernel has some bugs that need to be rectified as well.

Consider this a serious recommendation - install a copy of BOCHs and use its debugger rather than QEMU. BOCHs is ideal for bootloaders as it properly deals with 20-bit segment:offset addressing. For any real mode code BOCHs is an excellent tool. Learning to use a debugger properly allows you to see what is in the registers; examine memory, set breakpoints etc. You should be able to discover the bugs identified in this answer with some experience.


Problems in boot.asm

The bug in lba_to_hts can be seen here:

lba_to_hts:
    push ax
    push bx

    ...

    pop ax
    pop bx

You push AX then BX on the stack at the start, but you need to pop them off in reverse order. It should be:

push ax
push bx

...

pop bx
pop ax

In next_cluster you have a problem with this line:

mov ax, word [ds:si]

You have computed the offset in the FAT (FAT12) table where the next cluster can be found. The problem is that DS is not pointing to the segment where the FAT table is in memory, it is set at 0000h. You can't use:

mov ax, word [es:si]

because you have set ES to the kernel load segment (LOAD_SEG = 1000h). You can choose to save the DS register (push on stack), load DS with BUFFER_SEG. Then you could use:

mov ax, word [ds:si]

You would then have to restore DS when next_cluster is finished by POPing the old value from the stack. It should be noted that mov ax, word [ds:si] is the same as mov ax, word [si] except that the DS prefix will be output on the instruction unnecessarily. If the registers in the memory operand don't include BP then the memory access is done via DS implicitly, otherwise it is implicitly SS. Rule of thumb: don't add unnecessary overrides as they will increase code size.

I don't recommend this approach. The simplest way of fixing this is to place BUFFER_OFF in the same segment as the bootloader (segment 0000h). There is 32KiB of free memory from 0000h:8000h through to 0000h:0ffffh. If you modify your code to load the FAT and the root directory structures into 0000h:8000h then you can access the bootloader data, the FAT structure and the root directory entries via DS. When you load the kernel you can switch ES to LOAD_SEG.

There is another problem in this code:

finish_load:
    mov word [cluster], ax
    cmp ax, 0FF8h
    jae .jump_to_file
    add word [pointer], 512
    jmp next_cluster

You check to see if you have reached the last cluster for the file by comparing it with 0FF8h. If it is less than 0FF8h you add 512 to [pointer] to advance to the next offset in the buffer to read to. The problem is that jmp next_cluster doesn't go back to read the next cluster! jmp next_cluster should be jmp load_sector


In load_file you have this code:

load_file:
    mov si, load_file_str
    call print
    mov ax, LOAD_SEG
    mov es, ax
    xor bx, bx
    mov ah, 2
    mov al, 1


.load_sector:
    mov ax, word [cluster]
    add ax, 31
    call lba_to_hts
    mov ax, LOAD_SEG
    mov es, ax
    mov bx, word [pointer]
    pop ax
    push ax
    ;stc
    int 13h
    jnc next_cluster
    call reset
    jmp .load_sector

next_cluster:

Just before the .load_sector label you set the AX and BX registers for the Int 13h BIOS call. Unfortunately you clobber AX and BX in the lines just after the label .load_sector:. There is also an unusual POP/PUSH in the middle that makes no sense. You could alter this section of code to:

load_file:
    mov si, load_file_str
    call print
    mov ax, LOAD_SEG           ; ES=load segment for kernel
    mov es, ax

load_sector:
    mov ax, word [cluster]     ; Get cluster number to read
    add ax, 33-2               ; Add 31 to cluster since FAT data area
                               ; starts at Logical Block Address (LBA) 33
                               ; and we need to subtract 2 since valid
                               ; cluster numbers start at 2
    call lba_to_hts

    mov bx, word [pointer]     ; BX=Current offset in buffer to read to
    mov ax, 201h               ; AH=2 is read, AL=1 read 1 sector

    ;stc
    int 13h
    jnc next_cluster
    call reset
    jmp load_sector

next_cluster:

The revised version of the code would look like:

%define BUFFER_OFF 0x8000
%define BUFFER_SEG 0x0000
%define LOAD_SEG 0x1000
%define LOAD_OFF 0x0000

[bits 16]
[org 0x7c00]

jmp short start
nop

;DISK DESCRIPTION(BIOS PARAMETER BLOCK)
OEMLabel        db "BOOT    "
BytesPerSector      dw 512
SectorsPerCluster   db 1
ReservedForBoot     dw 1
NumberOfFats        db 2
RootDirEntries      dw 224     ; Number of entries in root dir
                               ; (224 * 32 = 7168 = 14 sectors to read)
LogicalSectors      dw 2880
MediumByte      db 0F0h
SectorsPerFat       dw 9
SectorsPerTrack     dw 18      ; Sectors per track (36/cylinder)
Sides           dw 2
HiddenSectors       dd 0
LargeSectors        dd 0
DriveNo         dw 0
Signature       db 0x29
VolumeID        dd 00000000h
VolumeLabel     db "myOS       "
FileSystem      db "FAT12   "

;BOOTLOADER
start:
    xor ax, ax
    mov ds, ax
    cli
    mov ss, ax
    mov sp, 0x7c00
    sti
    cld
    mov [drive], dl

    mov si, BUFFER_SEG         ; ES=buffer segment. Only has to be set once
    mov es, si
    mov bx, BUFFER_OFF

load_root:
    mov ax, 19                 ; Root directory starts at LBA 19
    call lba_to_hts
    mov ax, (2<<8) | 14        ; Root directory for this media fits in 14 sectors
                               ; Combine 2 moves (AH/AL) into one
                               ; same as 'mov ah, 2' and 'mov al, 14'
    int 13h
    jc reset
    mov si, load_root_str
    call print

search_file:
    mov di, BUFFER_OFF
    mov cx, word [RootDirEntries]
    xor ax, ax
.loop_search:
    xchg cx, dx
    mov si, filename
    mov cx, 11
    rep cmpsb
    je file_found
    add ax, 32
    mov di, BUFFER_OFF
    add di, ax
    xchg dx, cx
    loop .loop_search
    jmp file_not_found

file_found:
    mov ax, word [di+15]       ; Buffer and Bootloader now in same segment DS
                               ; Don't need ES:
    mov [cluster], ax
    mov ax, 1
    call lba_to_hts
    mov bx, BUFFER_OFF
    mov ax, (2<<8) | 9         ; Combine 2 moves (AH/AL) into one
                               ; same as 'mov ah, 2' and 'mov al, 9'    

load_FAT:
    mov si, FAT_str
    call print
    int 13h
    jnc load_file
    call reset
    jnc load_FAT
    jmp disk_error

load_file:
    mov si, load_file_str
    call print
    mov ax, LOAD_SEG           ; ES=load segment for kernel
    mov es, ax

load_sector:
    mov ax, word [cluster]     ; Get cluster number to read
    add ax, 33-2               ; Add 31 to cluster since FAT data area
                               ; starts at Logical Block Address (LBA) 33
                               ; and we need to subtract 2 since valid
                               ; cluster numbers start at 2
    call lba_to_hts
    mov bx, word [pointer]     ; BX=Current offset in buffer to read to
    mov ax, (2<<8) | 1         ; AH=2 is read, AL=1 read 1 sector
                               ; Combine 2 moves (AH/AL) into one
                               ; same as 'mov ah, 2' and 'mov al, 1'
    int 13h
    jnc next_cluster
    call reset
    jmp load_sector

next_cluster:
    mov ax, [cluster]
    xor dx, dx
    mov bx, 3
    mul bx
    mov bx, 2
    div bx
    mov si, BUFFER_OFF
    add si, ax
    mov ax, word [si]
    or dx, dx
    jz .even
.odd:
    shr ax, 4
    jmp short finish_load
.even:
    and ax, 0FFFh

finish_load:
    mov word [cluster], ax
    cmp ax, 0FF8h
    jae .jump_to_file
    add word [pointer], 512    ; We haven't reached end of kernel. Add 512 for next read
    jmp load_sector            ; Go back and load the next sector
.jump_to_file:
    mov dl, byte [drive]
    jmp LOAD_SEG:LOAD_OFF

;SUBROUTINES
file_not_found:
    mov si, not_found_str
    call print
    jmp reboot
print:
    pusha
    mov ah, 0x0E
.next:
    lodsb
    cmp al,0
    je .done
    int 0x10
    jmp .next
.done:
    popa
    ret
lba_to_hts:
    push ax
    push bx
    mov bx, ax
    xor dx, dx
    div word [SectorsPerTrack]
    add dl, 1
    mov cl, dl
    mov ax, bx
    xor dx, dx
    div word [SectorsPerTrack]
    xor dx, dx
    div word [Sides]
    mov dh, dl
    mov ch, al
    pop bx                     ; Need to POP in reverse order to the pushes!
    pop ax
    mov dl, [drive]
    ret
reset:
    mov ah, 0
    int 13h                    ;reset disk
    jc disk_error              ;if failed jump to search fail
    ret
disk_error:
    mov si, disk_error_str
    call print
reboot:
    mov si, reboot_pmpt
    call print
    mov ax, 0
    int 16h
    mov ax, 0
    int 19h

;DATA
load_root_str db 'Loading Root',13,10,0
disk_error_str db 'Disk Error!',13,10,0
reboot_pmpt db 'PRESS A KEY TO REBOOT',13,10,0
not_found_str db 'KERNEL NOT FOUND',13,10,0
FAT_str db 'Loading FAT',13,10,0
load_file_str db 'Loading KERNEL',13,10,0
drive dw 0
cluster dw 0
pointer dw 0
filename db 'KERNEL  BIN',0
;PADDING AND SIGNATURE
times (510-($-$$)) db 0x00
dw 0AA55h

Problems in kernel.asm

You improperly set up the segment registers and the stack should be on an even byte boundary. If you set SP to zero the first push will subtract 2 from SP placing data at 0000-2=0fffe at the top of the segment. I would simply set ES=DS=FS=GS=SS to CS. Secondly when you do a HLT instruction it only halts until the next interrupt and then falls into the instruction after the HLT. If you want to HLT indefinitely turn off interrupts with CLI first. It is still a good idea to put HLT in a loop in case you received a Non-Maskable Interrupt (NMI) that isn't masked by CLI.

Your kernel could be altered in this way:

[bits 16]               ;16-bit binary format

;VECTORS
os_vectors:
    jmp os_main

;KERNEL
os_main:
    mov ax, cs      ;CS is segment where we were loaded
    cli             ;clear interrupts
    mov ss, ax      ;set stack segment and pointer
    xor sp, sp      ;SP=0. First push will wrap SP to 0fffeh
    sti             ;restore interrupts
    cld             ;set RAM direction(for strings)
    mov ds, ax      ;DS=ES=FS=GS=CS
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov si, hello   ;print welcome
    call print_string
    cli             ;Turn off interrupts so that HLT doesn't continue
                    ;when an interrupt occurs
.hlt_loop:
    hlt
    jmp .hlt_loop   ; Infinite loop to avoid NMI dropping us into the code of
                    ; print_string

;SUBROUTINES
print_string:
    mov ah, 0x0e

.next_char:
    lodsb
    cmp al,0
    je .done_print
    int 0x10
    jmp .next_char

.done_print:
    ret

;DATA
hello db 'Hello',0

Other Observations

There are a number of inefficiencies in your code but I'll address some of the bigger ones. Although your next_cluster code works it uses more registers than it needs to, and it is longer encoding wise in memory. In order to multiply any value by 3 you can multiply the value by 2 and add the original value to that. The formula would look like:

valtimes3 = (value * 2) + value

This is important because to multiply a value in a register by 2 you only need to shift the bits left 1 bit with the SHL instruction. Division by 2 is done by right shifting the bits in a register by 1 with SHR instruction. The advantage with SHR is that the bit you shift out of the register is placed in the Carry Flag (CF). If CF is set then the value was odd, and if it is clear then the number was even. The next_cluster code could look like:

next_cluster:
    mov bx, [cluster]          ; BX = current cluster number
    mov ax, bx                 ; AX = copy of cluster number
    shl bx, 1                  ; BX = BX * 2
    add bx, ax                 ; BX = BX + AX (BX now contains BX * 3)
    shr bx, 1                  ; Divide BX by 2
    mov ax, [bx+BUFFER_OFF]    ; Get cluster entry from FAT table
    jnc .even                  ; If carry not set by SHR then result was even
.odd:
    shr ax, 4                  ; If cluster entry is odd then cluster number is AX >> 4
    jmp short finish_load
.even:
    and ah, 0Fh                ; If cluster entry is even then cluster number is AX & 0fffh
                               ; We just need to and AH with 0fh to achieve the same result
finish_load:

You can simplify the lba_to_hts by rearranging the standard calculation. I have written a previous Stackoverflow answer on doing just that and there is a drop in replacement using the revised formula:

lba_to_chs function that takes an LBA and converts it to CHS and only works for well known IBM compatible floppy disk formats.

;    Function: lba_to_chs
; Description: Translate Logical block address to CHS (Cylinder, Head, Sector).
;              Works **ONLY** for well known IBM PC compatible **floppy disk formats**.
;
;   Resources: http://www.ctyme.com/intr/rb-0607.htm
;              https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
;              https://stackoverflow.com/q/45434899/3857942
;              Sector    = (LBA mod SPT) + 1
;              Head      = (LBA / SPT) mod HEADS
;              Cylinder  = (LBA / SPT) / HEADS
;
;      Inputs: SI = LBA
;     Outputs: DL = Boot Drive Number
;              DH = Head
;              CH = Cylinder
;              CL = Sector
;
;       Notes: Output registers match expectation of Int 13h/AH=2 inputs
;
lba_to_chs:
    push ax                    ; Preserve AX
    mov ax, si                 ; Copy 16-bit LBA to AX
    div byte [SectorsPerTrack] ; 16-bit by 8-bit DIV : LBA / SPT
    mov cl, ah                 ; CL = S = LBA mod SPT
    inc cl                     ; CL = S = (LBA mod SPT) + 1
    xor ah, ah                 ; Upper 8-bit of 16-bit value set to 0 for DIV
    div byte [NumberOfHeads]   ; 16-bit by 8-bit DIV : (LBA / SPT) / HEADS
    mov ch, al                 ; CH = C = (LBA / SPT) / HEADS
    mov dh, ah                 ; DH = H = (LBA / SPT) mod HEADS
    mov dl, [boot_device]      ; boot device, not necessary to set but convenient
    pop ax                     ; Restore scratch register
    ret

You just need to change the name of the function to lba_to_hts; change NumberOfHeads to Sides; change boot_drive to drive; and alter the code so the LBA is passed in via AX rather than SI. AX doesn't even need to be preserved the way your existing code is written.


When you find that you need to read another cluster into memory you effectively add 512 to [pointer] to go to the next position in memory. The problem is that you limit yourself to a kernel that is 65536 bytes long. Once you reach 128 512-byte sectors read into memory you will exceed 65536 (128*512=65536). [pointer] will wrap and start back at 0 and you'll overwrite the part of the kernel you have already read. You can fix this issue by always doing a disk read to offset 0 (BX=0) and add 32 to ES. When you add 1 to a segment register you advance 16 bytes (a paragraph) in memory. If you want to advance 512 bytes you add 32 to ES (32*16=512). In your load_sectors code you can change:

    call lba_to_hts
    mov bx, word [pointer]     ; BX=Current offset in buffer to read to

to:

    call lba_to_hts
    xor bx, bx

In Finish_load you can change:

finish_load:
    mov word [cluster], ax
    cmp ax, 0FF8h
    jae .jump_to_file
    add word [pointer], 512    ; We haven't reached end of kernel. Add 512 for next read
    jmp load_sector            ; Go back and load the next sector
.jump_to_file:
    mov dl, byte [drive]
    jmp LOAD_SEG:LOAD_OFF

to:

finish_load:
    mov word [cluster], ax
    cmp ax, 0FF8h
    jae .jump_to_file
    mov ax, es
    add ax, 32                 ; Increasing segment by 1 advances 16 bytes (paragraph)
                               ; in memory. Adding 32 is same advancing 512 bytes (32*16)
    mov es, ax                 ; Advance ES to point at next 512 byte block to read into
    jmp load_sector            ; Go back and load the next sector
.jump_to_file:
    mov dl, byte [drive]
    jmp LOAD_SEG:LOAD_OFF

A version of boot.asm that implements these changes could look like:

%define BUFFER_OFF 0x8000
%define BUFFER_SEG 0x0000
%define LOAD_SEG 0x1000
%define LOAD_OFF 0x0000

[bits 16]
[org 0x7c00]

jmp short start
nop

;DISK DESCRIPTION(BIOS PARAMETER BLOCK)
OEMLabel        db "BOOT    "
BytesPerSector      dw 512
SectorsPerCluster   db 1
ReservedForBoot     dw 1
NumberOfFats        db 2
RootDirEntries      dw 224     ; Number of entries in root dir
                               ; (224 * 32 = 7168 = 14 sectors to read)
LogicalSectors      dw 2880
MediumByte      db 0F0h
SectorsPerFat       dw 9
SectorsPerTrack     dw 18      ; Sectors per track (36/cylinder)
Sides           dw 2
HiddenSectors       dd 0
LargeSectors        dd 0
DriveNo         dw 0
Signature       db 0x29
VolumeID        dd 00000000h
VolumeLabel     db "myOS       "
FileSystem      db "FAT12   "

;BOOTLOADER
start:
    xor ax, ax
    mov ds, ax
    cli
    mov ss, ax
    mov sp, 0x7c00
    sti
    cld
    mov [drive], dl

    mov si, BUFFER_SEG         ; ES=buffer segment. Only has to be set once
    mov es, si
    mov bx, BUFFER_OFF

load_root:
    mov ax, 19                 ; Root directory starts at LBA 19
    call lba_to_hts
    mov ax, (2<<8) | 14        ; Root directory for this media fits in 14 sectors
                               ; Combine 2 moves (AH/AL) into one
                               ; same as 'mov ah, 2' and 'mov al, 14'
    int 13h
    jc reset
    mov si, load_root_str
    call print

search_file:
    mov di, BUFFER_OFF
    mov cx, word [RootDirEntries]
    xor ax, ax
.loop_search:
    xchg cx, dx
    mov si, filename
    mov cx, 11
    rep cmpsb
    je file_found
    add ax, 32
    mov di, BUFFER_OFF
    add di, ax
    xchg dx, cx
    loop .loop_search
    jmp file_not_found

file_found:
    mov ax, word [di+15]       ; Buffer and Bootloader now in same segment DS
                               ; Don't need ES:
    mov [cluster], ax
    mov ax, 1
    call lba_to_hts
    mov bx, BUFFER_OFF
    mov ax, (2<<8) | 9         ; Combine 2 moves (AH/AL) into one
                               ; same as 'mov ah, 2' and 'mov al, 9'    

load_FAT:
    mov si, FAT_str
    call print
    int 13h
    jnc load_file
    call reset
    jnc load_FAT
    jmp disk_error

load_file:
    mov si, load_file_str
    call print
    mov ax, LOAD_SEG           ; ES=load segment for kernel
    mov es, ax

load_sector:
    mov ax, word [cluster]     ; Get cluster number to read
    add ax, 33-2               ; Add 31 to cluster since FAT data area
                               ; starts at Logical Block Address (LBA) 33
                               ; and we need to subtract 2 since valid
                               ; cluster numbers start at 2
    call lba_to_hts
    xor bx, bx                 ; Always read a kernel sector to offset 0

    mov ax, (2<<8) | 1         ; AH=2 is read, AL=1 read 1 sector
                               ; Combine 2 moves (AH/AL) into one
                               ; same as 'mov ah, 2' and 'mov al, 1'
    int 13h
    jnc next_cluster
    call reset
    jmp load_sector

next_cluster:
    mov bx, [cluster]          ; BX = current cluster number
    mov ax, bx                 ; AX = copy of cluster number
    shl bx, 1                  ; BX = BX * 2
    add bx, ax                 ; BX = BX + AX (BX now contains BX * 3)
    shr bx, 1                  ; Divide BX by 2
    mov ax, [bx+BUFFER_OFF]    ; Get cluster entry from FAT table
    jnc .even                  ; If carry not set by SHR then result was even
.odd:
    shr ax, 4                  ; If cluster entry is odd then cluster number is AX >> 4
    jmp short finish_load
.even:
    and ah, 0Fh                ; If cluster entry is even then cluster number is AX & 0fffh
                               ; We just need to and AH with 0fh to achieve the same result
finish_load:
    mov word [cluster], ax
    cmp ax, 0FF8h
    jae .jump_to_file
    mov ax, es
    add ax, 32                 ; Increasing segment by 1 advances 16 bytes (paragraph)
                               ; in memory. Adding 32 is same advancing 512 bytes (32*16)
    mov es, ax                 ; Advance ES to point at next 512 byte block to read into
    jmp load_sector            ; Go back and load the next sector
.jump_to_file:
    mov dl, byte [drive]
    jmp LOAD_SEG:LOAD_OFF

;SUBROUTINES
file_not_found:
    mov si, not_found_str
    call print
    jmp reboot
print:
    pusha
    mov ah, 0x0E
.next:
    lodsb
    cmp al,0
    je .done
    int 0x10
    jmp .next
.done:
    popa
    ret

;    Function: lba_to_hts
; Description: Translate Logical block address to CHS (Cylinder, Head, Sector).
;              Works ONLY for well known IBM PC compatible floppy disk formats.
;
;   Resources: http://www.ctyme.com/intr/rb-0607.htm
;              https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
;              https://stackoverflow.com/q/45434899/3857942
;              Sector    = (LBA mod SPT) + 1
;              Head      = (LBA / SPT) mod HEADS
;              Cylinder  = (LBA / SPT) / HEADS
;
;      Inputs: AX = LBA
;     Outputs: DL = Boot Drive Number
;              DH = Head
;              CH = Cylinder
;              CL = Sector
;
;       Notes: Output registers match expectation of Int 13h/AH=2 inputs
;
lba_to_hts:
    div byte [SectorsPerTrack] ; 16-bit by 8-bit DIV : LBA / SPT
    mov cl, ah                 ; CL = S = LBA mod SPT
    inc cl                     ; CL = S = (LBA mod SPT) + 1
    xor ah, ah                 ; Upper 8-bit of 16-bit value set to 0 for DIV
    div byte [Sides]           ; 16-bit by 8-bit DIV : (LBA / SPT) / HEADS
    mov ch, al                 ; CH = C = (LBA / SPT) / HEADS
    mov dh, ah                 ; DH = H = (LBA / SPT) mod HEADS
    mov dl, [drive]            ; boot device, not necessary to set but convenient
    ret

reset:
    mov ah, 0
    int 13h                    ;reset disk
    jc disk_error              ;if failed jump to search fail
    ret
disk_error:
    mov si, disk_error_str
    call print
reboot:
    mov si, reboot_pmpt
    call print
    mov ax, 0
    int 16h
    mov ax, 0
    int 19h

;DATA
load_root_str db 'Loading Root',13,10,0
disk_error_str db 'Disk Error!',13,10,0
reboot_pmpt db 'PRESS A KEY TO REBOOT',13,10,0
not_found_str db 'KERNEL NOT FOUND',13,10,0
FAT_str db 'Loading FAT',13,10,0
load_file_str db 'Loading KERNEL',13,10,0
drive dw 0
cluster dw 0
filename db 'KERNEL  BIN',0
;PADDING AND SIGNATURE
times (510-($-$$)) db 0x00
dw 0AA55h

When run in QEMU this is what is displayed:

enter image description here

Upvotes: 3

Related Questions