I am building an operating system and am experiencing a problem where the bootloader successfully prints its message, but the kernel's message isn't displayed. Here's a breakdown:
Setup:
Assembler and Environment:
- NASM with -f bin option
- Tested on qemu-system-i386
- Environment: Windows Subsystem for Linux (WSL) with Xlaunch (though I suspect this isn't relevant)
Quick Note
push ax
push bx
mov bl, dl ; This is to preserve dl
push bx
This code snippet is part of a bootloader, and it might seem confusing initially.
The requirement here is to preserve the dl register throughout the function. You might ask:
- Why not just
push dlonto the stack?- The reason is the stack only works with 16-bit values. dl is an 8-bit register, so pushing it directly isn't possible.
- Why not
push dx?- This is because
dhserves as a return value in this context.
- This is because
To work around these constraints, I took this approach:
- Pushed bx onto the stack.
- Moved the value of
dl into the lower bits of bx(i.e., bl). - Pushed bx (
now holding the value of dl) onto the stack again. - Later, to restore the value, I simply popped bx and then
mov dl, bl.
Files:
bootloader.asm
[org 0x7c00]
[bits 16]
jmp main
_message_read_err: db 'Error in reading floppy!', 0
_message_boot: db 'Booting SSoS...', 0
_sectors_per_track: dw 18
_head_count: dw 2
_kernel_start_LBA: dw 1
_kernel_size: dw 1
_stack_ptr_addr: dw 0x7c00
_es_start: dw 0x7c0
; Arguments:
; None
; Returns:
; None
main:
mov ax, 0 ; can't write to ds and ss directly
mov ds, ax
mov ss, ax
mov sp, [_stack_ptr_addr] ; stack pointer
mov ax, [_es_start] ; address * 16 = real address -> 0x7c0 * 16 (dec) = 0x7c00
mov es, ax
mov si, _message_boot
call puts
; Converting LBA to CHS
;; Parameters
mov si, 1
call LBA_to_CHS ; after this call, CH, CL, and DH will have cylinder, sector, and head values
mov bx, 0x200
mov al, [_kernel_size] ; number of sectors to read
call read_disk
jmp 0:7e00h
; Arguments:
; si - Points to the string to be printed
; Returns:
; None
puts:
push ax
.begin:
mov ah, 0x0E
lodsb
cmp al, 0
je .done
int 10h
jmp .begin
.done:
pop ax
ret
; Arguments:
; si - LBA adress
; Returns:
; ch, dh, cl - CHS adress
;; dx and cx will have to be modified
LBA_to_CHS:
push ax
push bx
mov bl, dl ; This is to preserve dl
push bx
mov ax, [_head_count]
mul word [_sectors_per_track]
mov bx, ax
mov ax, si
mul word bx
mov ch, al ; Put lower bits of ax as the cylinder address
mov ax, si
div word [_sectors_per_track] ; result in dx:ax
div word [_head_count] ; result also in dx:ax (dx is remainder)
mov dh, dl ; since dx is composed of dh (higher bits) and dl (lower bits) we want to move the lower bits into dh
push dx
mov ax, si
div word [_sectors_per_track] ; remainder in dx
inc dx
mov cl, dl
pop dx
pop bx
mov dl, bl
pop bx
pop ax
ret
; Arguments:
; bx - Address to load the data
; ch, cl, dh - CHS values
; Returns:
; None
read_disk:
push ax
push si
mov si, 4
.retry:
dec si
cmp si, 0
je .read_error
mov ah, 02h
mov al, [_kernel_size]
int 13h
jc .retry
pop si
pop ax
ret
.read_error:
mov si, _message_read_err
call puts
cli
hlt
times 510-($-$$) db 0
dw 0aa55h
kernel.asm
[org 0x7E00]
[bits 16]
jmp main
_message: db 'Hello from kernel!', 0
main:
mov si, _message
call puts
cli
hlt
puts:
mov ah, 0x0E
.begin:
lodsb
cmp al, 0
je .done
int 10h
jmp .begin
.done:
ret
Makefile
BUILD_DIR = build
SRC_DIR = src
IMG_NAME = dev
dev: image
qemu-system-i386 -fda $(BUILD_DIR)/$(IMG_NAME).img
image: $(BUILD_DIR)/bootloader.bin $(BUILD_DIR)/kernel.bin
@dd if=$(BUILD_DIR)/bootloader.bin of=$(BUILD_DIR)/$(IMG_NAME).img bs=512
@dd if=$(BUILD_DIR)/kernel.bin of=$(BUILD_DIR)/$(IMG_NAME).img bs=512 seek=1
@truncate -s 1440k $(BUILD_DIR)/$(IMG_NAME).img
@hexdump -C $(BUILD_DIR)/$(IMG_NAME).img
@objdump -b binary -m i8086 -M intel -D build/dev.img
clean:
rm -f $(BUILD_DIR)/bootloader.bin $(BUILD_DIR)/kernel.bin
$(BUILD_DIR)/%.bin: $(SRC_DIR)/%.asm
nasm $^ -f bin -o $@
Specifics on Constants in bootloader.asm:
- _stack_ptr_addr: Set to 0x7b00 because it's 256 bytes under the bootloader. I believe this is a safe location since the stack grows downwards.
- _es_start: Set to 0x7c0 so the actual physical address (in real mode) equals 0x7c00 — the bootloader's load address. I offset by 0x200 when jumping to the kernel, which matches the kernel's load address.
for clarity shows that the first instruction appears at byte 0x200.
The issue:
Upon booting, the message "Booting SSoS..." is displayed, but the kernel's "Hello from kernel!" message isn't.
Observations & Assumptions:
Someone also had the same problem here. I tried removing the mov dl, 0 in the read_disk function but it still didin't work.
I suspect the issue might be in the read_disk function within the bootloader.
Even though no error message displays, the qemu-system-i386 command seems unusual.
Constants like _sectors_per_track and drive number (register dl in read_disk function) seem accurate. However, even when I attempted different disk-reading methods, the problem was still occurring.
Seeking Help:
I know this is a complex and low-level topic which requires a deep understanding of operating systems. That's why I thank everyone who will try to help me. Thanks a lot!
I tried different methods of reading the floppy disk (like reading directly with CHS adressing) however none of them worked. I also double-checked my constants and looked on the disassebly of the image but everything seems to be right. I looked into others code but mine seems (almost except it's not that perfect) identical.

jmp far [es:bx]is wrong as it is an indirect jump. Also even if it were direct that would jump to7c0h:200hbut kernel.asm hasorg 7e00h. Just dojmp 7e00h(if you are happy with a hardcoded address).jmp 7E00hbecausecscan be 7C0h. Better harden it to a far immediate jump likejmp 0:7E00hss:spto 0:7C00h is fine, though your choice also works. And if you have no other use for it, the variable_stack_ptr_addrisn't needed, just writemov sp, 7C00h. You did get the stack init right so as to loadssand then in the very next instruction setsp.LBA_to_CHSdoes not preservedlso inread_diskyou must restore or resetdl. Forcing it to zero will hardcode the first diskette unit, fda, to load from.jc .retryloop inread_diskdoesn't make any sense. You try to useaxas a retry counter but you do not preserve it across the function call. And your attempt to initialise the retry counter always corrupts the kernel size in sectors that you passed inal. Are you aware that the 8-bitalandahcombine to the 16-bitax? If you write toaxyou're also writing toalandah. For the retry counter, use another register not used by theint 13hcall (egsi,di,bp), or a memory variable, or properly preserve the counter value usingpushandpop.lba_to_chsfunction in assembler: stackoverflow.com/a/45495410/3857942