I am trying to write my own bootloader. While it works fine in QEMU, Bochs and VirtualBox, I cannot seem to make it work on my laptop.
On my laptop, the bootloader behaves very differently to all emulators, hanging seemingly random places, refusing to print, even skipping some jmp $
instructions.
While I have a lot of trouble with "real-hardware", I figure there is one cause to them all.
The following code is a short bootloader that should print a "TEST" message 3 times, then hang by jumping to the same location:
[BITS 16]
[ORG 0x7C00]
jmp 0x0000:start_16 ; In case bootloader is at 0x07C0:0x0000
start_16:
xor ax, ax
mov ds, ax
mov es, ax
cli ; Disable interrupts
mov ss, ax
mov sp, 0x7C00
sti ; Enable interrupts
cld ; Clear Direction Flag
; Store the drive number
mov [drive_number], dl
; Print message(s)
mov si, msg
call print_string
mov si, msg
call print_string
mov si, msg
call print_string
jmp $ ; HALT
; print_string
; si = string
print_string:
pusha
mov ah, 0x0E
.repeat:
lodsb
cmp al, 0x00
je .done
int 0x10
jmp short .repeat
.done:
popa
ret
; Variables
drive_number db 0x00
msg db 'TEST', 0x0D, 0x0A, 0x00
times 510-($-$$) db 0x00
db 0x55
db 0xAA
Compile and emulate the code with:
$ nasm -f bin bootloader.asm
$ qemu-system-x86_64 bootloader
On emulators, it prints "TEST" three times and hangs, On my laptop, it prints "TEST" followed by 3 weird characters:
Most bootloader code from http://wiki.osdev.org does not work either. For example, none of the code-snippets from http://wiki.osdev.org/Babystep2 work on my laptop.
What is wrong with my code? How can I fix it?
If I remove the 2 unnecessary mov si, msg
, the "TEST" message is printed twice.
Laptop:
Assembly and writing:
$ nasm -f bin bootloader.asm
$ qemu-system-x86_64 bootloader # TEST 1
$ sudo dd if=/dev/zero of=/dev/sdd bs=1M count=1 # clean the USB
$ sudo dd if=bootloader of=/dev/sdd conv=fsync # write to USB
$ qemu-system-x86_64 /dev/sdd # TEST 2
Ross Ridge noticed in the comments that Ω♣|
are the first 3 bytes of the bootloader.
Updated print function and string:
print_string:
pusha
.repeat:
mov ah, 0x0E
xor bx, bx
cld ; Clear Direction Flag
lodsb
cmp al, 0x00
je .done
int 0x10
jmp short .repeat
.done:
popa
ret
msg db 'TEST', 0x00
Output
A single TEST
. Additional two are missing.
As suggested by Ross Ridge, dumpregs
have been added for better debugging. The int 0x10
does not modify any registers.
After some testing, I have moved the dumpregs
function around drive_number
assignment and a jmp $
just after. The code should print 1 line of register dump and halt. Instead, it continues:
The full code: https://gist.github.com/anonymous/0ddc146f73ff3a13dd35
Disassembly of the current bootloader using:
$ ndisasm -b16 bootload2 -o 0x7c00
https://gist.github.com/anonymous/c9384fbec25513e3b815
It's now looking like the BIOS maybe modifying the BIOS parameter block that it incorrectly assumes is a part of the bootsector loaded into memory. It might feel it needs to because the geometry it gives to the USB device when booting it might differ from the geometry used when the bootsector and its presumed BPB was written to the device. Since the presence of a jump instruction at the start of a bootsector is one of the ways applications test for the existence of a BPB, you can try inserting some other instruction (but not a NOP) at the start of your bootsector. For example:
[BITS 16]
[ORG 0x7C00]
xor ax,ax
jmp 0x0000:start_16 ; In case bootloader is at 0x07C0:0x0000
start_16:
Note that a far jump doesn't appear to be one of the normal indicators of the presence of a BPB. In fact it's pretty conclusive evidence that a BPB isn't present, as the BPB starts at offset 4 and a far jump instruction is 5 bytes long.
If that doesn't work you can try reserving space for a BPB, something like this:
[BITS 16]
[ORG 0x7C00]
jmp start
nop
resb 8 + 25
start:
jmp 0x0000:start_16 ; In case bootloader is at 0x07C0:0x0000
start_16:
Here's some code you can use to dump registers to help debug the problem:
dumpregs:
push es
pusha
push 0xb800
pop es
mov di, [vidmem_ptr]
mov bp, sp
mov cx, 8
dump_loop:
dec bp
dec bp
mov ax, [bp + 16]
call printhex2
inc di
inc di
loop dump_loop
mov [vidmem_ptr], di
popa
pop es
ret
printhex2:
push ax
mov al, ah
call convhex1
pop ax
convhex1:
aam 16
; DB 0D4h, 16
xchg al, ah
call convhex
mov al, ah
convhex:
cmp al, 10
jb lessthan_10
add al, 'A' - '0' - 10
lessthan_10:
add al, '0'
stosb
mov al, 7
stosb
ret
vidmem_ptr dw 5 * 80 * 2 ; start at row 5
It uses direct video writes to dump all the general purpose registers. It uses the PUSHA/POPA register order: AX CX DX BX SP BP SI DI
If you bracket the INT 0x10 instruction in your original code with calls to this function, you should get output like this:
In particular you want to make sure the 8 numbers of the left hand side match the 8 numbers on the right hand side for each of lines of debug output. The BIOS call you're using shouldn't be changing any registers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With