I tried to load kernel stored on a floppy disk formatted to FAT12. When I try to read sectors where the kernel is located, I don't have any errors (carry flag is not set), but the memory where I try to load the kernel has zeroes.
org 0x7C00
bits 16
%define endl 0x0D, 0x0A
%define dirEntrySize 32
%define fatLocation 0x7E00
%define rootdirLocation 0x9000
%define kernelLocation 0x9E00
%define filenameLength 11
jmp short main
nop
bpb_oemId: db "mkfs.fat"
bpb_bytesPerSector: dw 512
bpb_sectorsPerCluster: db 1
bpb_reservedSectors: dw 1
bpb_fatCount: db 2
bpb_rootdirEntries: dw 224
bpb_sectorCount: dw 2880
bpb_mediaDescriptor: db 0xF0
bpb_sectorsPerFat: dw 9
bpb_sectorsPerTrack: dw 18
bpb_headCount: dw 2
bpb_hiddenSectors: dd 0
bpb_largeSectorCount: dd 0
ebr_driveNumber: db 0x00
ebr_ntFlags: db 0
ebr_driveSignature: db 0x29
ebr_volumeId: dd 0
ebr_volumeLabel: db "NO NAME "
ebr_systemId: db "FAT12 "
rootdirStart: dw 0
dataStart: dw 0
main:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
mov [ebr_driveNumber], dl
mov si, msg_loading
call puts
; Read FAT
mov ax, [bpb_reservedSectors]
mov bx, fatLocation
mov cx, [bpb_sectorsPerFat]
call readSectors
; Read Root Directory
mov ax, [bpb_rootdirEntries]
mov bx, dirEntrySize
mul bx
div word [bpb_bytesPerSector]
mov [dataStart], ax
xchg ax, cx
mul byte [bpb_fatCount]
add ax, [bpb_reservedSectors]
mov bx, rootdirLocation
call readSectors
mov [rootdirStart], ax
add [dataStart], ax
; Find and read the kernel
mov si, kernelFilename
call findFile
mov bx, kernelLocation
call readFile
; Load kernel
mov dl, [ebr_driveNumber]
jmp kernelLocation
cli
hlt
putc:
push ax
push bx
mov ah, 0x0E
xor bx, bx
int 0x10
pop bx
pop ax
ret
puts:
push ax
push si
.loop:
lodsb
test al, al
jz .done
call putc
jmp .loop
.done:
pop si
pop ax
ret
lbaToChs:
push ax
push dx
xor dx, dx
div word [bpb_sectorsPerTrack]
inc dx
mov cx, dx
xor dx, dx
div word [bpb_headCount]
mov dh, dl
mov ch, al
shl ah, 6
or cl, ah
pop ax
mov dl, al
pop ax
ret
readSectors:
push ax
push cx
push dx
push si
push di
push cx
call lbaToChs
pop si
mov di, 3
mov dl, [ebr_driveNumber]
.loop:
mov ah, 0x02
int 0x13
mov ah, 0x01
int 0x13
jnc .done
mov ah, 0x00
int 0x13
dec di
jz .fail
dec si
jnz .loop
.done:
pop di
pop si
pop dx
pop cx
pop ax
ret
.fail:
mov si, msg_diskError
call puts
cli
hlt
findFile:
push bx
push si
push di
mov ax, 0
mov di, rootdirLocation
.loop:
mov bx, filenameLength
.compare:
cmpsb
jnz .skip
dec bx
jz .done
jmp .compare
.skip:
sub si, filenameLength
add di, dirEntrySize - filenameLength
inc ax
cmp ax, word [bpb_rootdirEntries]
jnz .loop
.fail:
mov si, msg_findFailed
call puts
cli
hlt
.done:
mov si, rootdirLocation
mov bx, dirEntrySize
mul dx
add si, ax
add si, 0x1A
lodsw
pop di
pop si
pop bx
ret
readFile:
push ax
push bx
push cx
push dx
.loop:
push ax
sub ax, 2
mul byte [bpb_sectorsPerCluster]
add ax, word [dataStart]
mov cx, 1
call readSectors
mov ax, [bpb_bytesPerSector]
mul byte [bpb_sectorsPerCluster]
add bx, ax
pop ax
mov cl, 3
mul cx
dec cl
div cx
mov si, fatLocation
add si, ax
mov ax, [si]
or dx, dx
jnz .odd
.even:
and ax, 0x0FFF
jmp .update
.odd:
shr ax, 4
.update:
cmp ax, 0xFF0
jnl .loop
.done:
pop dx
pop cx
pop bx
pop ax
ret
.fail:
mov si, msg_loadFailed
call puts
cli
hlt
kernelFilename: db "KERNEL BIN"
msg_loading: db "Loading...", endl, 0
msg_findFailed: db "Cannot find kernel", 0
msg_loadFailed: db "Cannot load kernel", 0
msg_diskError: db "Disk error", 0
times 510+$$-$ db 0
dw 0xAA55
I tried debugging with GDB and checked the registers, but their value was correct. I also checked the memory where I stored the FAT and the Root Directory, but this data also was correct. Only the memory where I wanted to load kernel was filled with zeroes.
Register values after lba-to-chs conversion:
eax 0x21 33
ecx 0x10 16
edx 0x100 256
ebx 0x9e00 40448
esp 0x7be6 0x7be6
ebp 0x0 0x0
esi 0x7d98 32152
edi 0x0 0
eip 0x7ce6 0x7ce6
eflags 0x202 [ IOPL=0 IF ]
cs 0x0 0
ss 0x0 0
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
AX value is correct, should be lba of start of data: reserved_sectors + sectors_per_fat * fat_count + root_dir_entries * dir_entry_size / bytes_per_sectors = 1 + 9 * 2 + 224 * 32 / 512 = 1 + 18 + 14 = 33
. In floppy image opened with hex editor, that points to start of the kernel.
Loaded file allocation table:
0x7e00: 0xf0 0xff 0xff 0xff 0x0f 0x00 0x00 0x00
0x7e08: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
...
Loaded root directory:
0x9000: 0x4b 0x45 0x52 0x4e 0x45 0x4c 0x20 0x20
0x9008: 0x42 0x49 0x4e 0x20 0x18 0x00 0x28 0x61
0x9010: 0xaa 0x58 0xaa 0x58 0x00 0x00 0x28 0x61
0x9018: 0xaa 0x58 0x02 0x00 0x25 0x01 0x00 0x00
0x9020: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x9028: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x9030: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x9038: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
...
*Loaded* kernel:
0x9e00: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x9e08: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x9e10: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x9e18: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
...
org 0x7C00
bits 16
You could add cpu 8086
here to insure that conditional jumps use the short jump encodings only.
%define endl 0x0D, 0x0A
%define dirEntrySize 32
%define fatLocation 0x7E00
%define rootdirLocation 0x9000
%define kernelLocation 0x9E00
%define filenameLength 11
The FAT on a 1440 KiB diskette image has 9 sectors, 1200h bytes. So it just so fits in the memory you allocated. This would blow up if you wanted to boot off a larger file system.
A root directory with 224 entries needs 1C00h bytes. However, you don't actually need the root directory any longer after you found the file to search, so the overlap between directory location and kernel location is not a problem.
jmp short main
nop
bpb_oemId: db "mkfs.fat"
bpb_bytesPerSector: dw 512
bpb_sectorsPerCluster: db 1
bpb_reservedSectors: dw 1
bpb_fatCount: db 2
bpb_rootdirEntries: dw 224
bpb_sectorCount: dw 2880
bpb_mediaDescriptor: db 0xF0
bpb_sectorsPerFat: dw 9
bpb_sectorsPerTrack: dw 18
bpb_headCount: dw 2
bpb_hiddenSectors: dd 0
bpb_largeSectorCount: dd 0
ebr_driveNumber: db 0x00
ebr_ntFlags: db 0
ebr_driveSignature: db 0x29
ebr_volumeId: dd 0
ebr_volumeLabel: db "NO NAME "
ebr_systemId: db "FAT12 "
rootdirStart: dw 0
dataStart: dw 0
For size optimisation you don't want to put these as zero-initialised bytes into the loader because you never need them to be zero-initialised. However, while it is not optimal, this is not incorrect.
main:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
mov [ebr_driveNumber], dl
mov si, msg_loading
call puts
; Read FAT
mov ax, [bpb_reservedSectors]
mov bx, fatLocation
mov cx, [bpb_sectorsPerFat]
call readSectors
; Read Root Directory
mov ax, [bpb_rootdirEntries]
mov bx, dirEntrySize
mul bx
div word [bpb_bytesPerSector]
This should round up the sectors per root directory. However, a lot of loaders and drivers make this error.
mov [dataStart], ax
This is what I listed for the variables, you unconditionally overwrite it so it needn't be zero-initialised.
xchg ax, cx
mul byte [bpb_fatCount]
Assumes that sectors per FAT is < 256.
add ax, [bpb_reservedSectors]
Assumes that the root directory start is < 65_536.
mov bx, rootdirLocation
call readSectors
mov [rootdirStart], ax
You don't seem to read this variable anywhere so why set it?
add [dataStart], ax
; Find and read the kernel
mov si, kernelFilename
call findFile
As find file never is called with another name, you can hardcode the kernel filename address within the find file function.
mov bx, kernelLocation
call readFile
; Load kernel
mov dl, [ebr_driveNumber]
jmp kernelLocation
The comment is incorrect, at this point the kernel is already loaded (if the code works correctly). What you're trying to do here is transfer control to the kernel.
You're assuming cs:ip
equal to 0:7C00h at the beginning of your loader. It would be better to make a far jump like jmp 0:kernelLocation
instead to insure cs
is reset to zero, because the prior loader may enter your code at 7C0h:0 instead. (Immediate short and near calls and jumps are always done using rel8 or rel16 so they're not affected by this difference.)
cli
hlt
This is dead code, never reached. Remove it to save some space.
putc:
push ax
push bx
mov ah, 0x0E
xor bx, bx
int 0x10
pop bx
pop ax
ret
If you used bp
you should also push and pop it here because this function may destroy bp
.
puts:
push ax
push si
.loop:
lodsb
test al, al
jz .done
call putc
jmp .loop
.done:
pop si
pop ax
ret
The put character function doesn't need to be a function if you only ever call it from one spot. This is not a correctness problem however.
lbaToChs:
push ax
push dx
xor dx, dx
div word [bpb_sectorsPerTrack]
inc dx
mov cx, dx
xor dx, dx
div word [bpb_headCount]
mov dh, dl
mov ch, al
shl ah, 6
or cl, ah
pop ax
mov dl, al
pop ax
ret
push dx
and the companion pop
and mov
are not needed as is because you always overwrite dl
after calling this function.
readSectors:
push ax
push cx
push dx
push si
push di
push cx
call lbaToChs
pop si
mov di, 3
mov dl, [ebr_driveNumber]
.loop:
mov ah, 0x02
int 0x13
You didn't initialise al
to the number of sectors to read! The naive solution would be to init it to the total number of sectors you want to read per readSectors
function call. However, if you cross a 64 KiB boundary or a "track" boundary then reading more than 1 sector may fail. Therefore in my boot sector loaders the read_sector
function reads a single sector unconditionally. It does the LBA to CHS calculation for every sector that it wants to read, rather than once per continuous read. The callers have to take care of looping if they want to read multiple sectors.
So if you wanted to do this you should do the LBA to CHS calculation in a single-sector read function, and call this in a loop which increments the LBA and adds to bx
after reading every single sector. (This would also mean you needn't calculate the bytes per cluster and add it to your pointer later in readFile
.)
mov ah, 0x01
int 0x13
jnc .done
This call is useless at best. The jnc
will jump if the read succeeded. But if that happens you will skip your dec si
loop so you never continue to read subsequent sectors.
mov ah, 0x00
int 0x13
dec di
jz .fail
dec si
jnz .loop
The dec si
loop is very confused. The dec di
loop appears to be intended as an error retry. But why loop for the next sector only if an error retry is happening? Also, if you do loop for si
> 1 then you do not calculate the next CHS tuple nor transfer address, so you cannot possibly read multiple sectors correctly.
.done:
pop di
pop si
pop dx
pop cx
pop ax
ret
.fail:
mov si, msg_diskError
call puts
cli
hlt
It is nicer to run int 16h
function 00h then int 19h
to return control to the prior loader. Or use sti
\ halt:
\ hlt
\ jmp halt
which allows to reboot eg using Ctrl-Alt-Del.
findFile:
push bx
push si
push di
mov ax, 0
mov di, rootdirLocation
.loop:
mov bx, filenameLength
.compare:
cmpsb
jnz .skip
dec bx
jz .done
jmp .compare
This loop works if the (SFN) KERNEL.BIN is in the very first directory entry.
.skip:
sub si, filenameLength
add di, dirEntrySize - filenameLength
This is very confused. You don't know at which point di
and si
are pointing (like if KERNEL.SYS
is found as a mismatch then di
will point at the Y
in "SYS" and si
at the I
in "BIN") so these hardcoded adjustments cannot possibly be correct. If I insert a deleted directory entry (first byte is a mismatch) your instructions here also do not work.
inc ax
cmp ax, word [bpb_rootdirEntries]
jnz .loop
This is odd, but not wrong. I prefer to initialise to root directory entries and count down in a register.
.fail:
mov si, msg_findFailed
call puts
cli
hlt
You can share most of the code for this with another error display function.
.done:
mov si, rootdirLocation
mov bx, dirEntrySize
mul dx
This seems wrong, perhaps you meant mul bx
?
add si, ax
add si, 0x1A
lodsw
pop di
pop si
pop bx
ret
readFile:
push ax
push bx
push cx
push dx
.loop:
push ax
sub ax, 2
mul byte [bpb_sectorsPerCluster]
This assumes your current cluster is < 256. This is not necessarily true on a 1440 KiB diskette image.
add ax, word [dataStart]
mov cx, 1
call readSectors
If readSectors
worked this would hardcode the cluster size to 1 sector.
mov ax, [bpb_bytesPerSector]
mul byte [bpb_sectorsPerCluster]
add bx, ax
This assumes Bytes per Sector < 256, which is not true on the typical image. So you will add zero to bx
, which is why you overwrite the first sector of the kernel later.
pop ax
mov cl, 3
mul cx
dec cl
div cx
mov si, fatLocation
add si, ax
mov ax, [si]
or dx, dx
jnz .odd
.even:
and ax, 0x0FFF
jmp .update
.odd:
shr ax, 4
Oh, this is a 186 instruction. Not a problem for qemu of course.
.update:
cmp ax, 0xFF0
jnl .loop
This should be jb .loop
(or jl .loop
). If the next cluster is below 0FF0h then loop.
.done:
pop dx
pop cx
pop bx
pop ax
ret
.fail:
mov si, msg_loadFailed
call puts
cli
hlt
This is dead code, never reached.
kernelFilename: db "KERNEL BIN"
msg_loading: db "Loading...", endl, 0
msg_findFailed: db "Cannot find kernel", 0
msg_loadFailed: db "Cannot load kernel", 0
msg_diskError: db "Disk error", 0
times 510+$$-$ db 0
dw 0xAA55
Finally, I debugged this using boot loaded lDebug running in qemu. I created a 1440 KiB diskette image using my bootimg script. And here's the command scriptlet I used to debug your loader:
test$ nasm kernel.asm -o kernel.bin && nasm test.asm -o test.bin -l test.lst && nasm -I ~/proj/lmacros/ -I ~/proj/bootimg/ ~/proj/bootimg/bootimg.asm -D_PAYLOADFILE="kernel.bin" -D_BOOTPATCHFILE="test.bin" -o diskette.img && nasm -I ~/proj/lmacros/ -I ~/proj/ldosboot/ ~/proj/ldosboot/boot.asm -o boot12.bin -D_LOAD_NAME="'LDEBUG'" && nasm -I ~/proj/lmacros/ -I ~/proj/ldosmbr/ ~/proj/ldosmbr/oldmbr.asm -o oldmbr.bin && nasm -I ~/proj/ldebug/bin/ -I ~/proj/lmacros/ -I ~/proj/bootimg/ ~/proj/bootimg/bootimg.asm -D_PAYLOADFILE="ldebug.com,extlib.eld" -D_BOOTPATCHFILE="boot12.bin" -D_MBR -D_MBRPATCHFILE="oldmbr.bin" -o hdimage.img && qemu-system-i386 -hda hdimage.img -fda diskette.img -boot order=c -display curses -chardev serial,id=serial2,path=/tmp/vptty-dos -serial null -serial chardev:serial2; stty sane
I changed a switch to -D_PAYLOADFILE="::directorypad,1,kernel.bin"
to insert a deleted directory entry, which makes your code fail even after correcting the jnl
error.