Writing a Bootloader for My 32-bit x86 OS: Common Pitfalls and How I Fixed Them
I decided to write my own 32-bit x86 operating system from scratch. The first step was a bootloader – the small piece of code that loads the kernel. It’s supposed to be simple, but I ran into several frustrating issues. This post documents the real problems I faced and how I solved them, so you can avoid the same traps.
1. The BIOS signature
The boot sector must end with the magic bytes 0x55AA. The BIOS checks these two bytes before loading the sector. I forgot them, and nothing happened.
Fix: Add dw 0xAA55 at the end of your bootloader code, after filling the rest of the 512-byte sector with zeros.
times 510-($-$$) db 0
dw 0xAA55
- Losing the boot drive number
The BIOS passes the disk number in the dl register. I accidentally overwrote it before using it in the int 0x13 call. The disk read failed silently.
Fix: Save dl right at the start:
mov [boot_drive], dl
Then restore or use the saved value when reading sectors.
- A20 line – the hidden gatekeeper
To access memory above 1 MB and enter 32‑bit protected mode, the A20 line must be enabled. I wasted hours trying to figure out why my far jump crashed. The quickest way (though not the most universal) is to use port 0x92.
in al, 0x92
or al, 2
out 0x92, al
Many emulators enable A20 by default, but real hardware doesn’t – always include this step.
- Global Descriptor Table (GDT) mistakes
A flat memory model requires at least two descriptors: code and data. I messed up the segment limits and flags, causing general protection faults.
Correct minimal GDT for flat model (32‑bit):
gdt_start:
dq 0 ; null descriptor
dw 0xFFFF, 0x0000, 0x9A00, 0x00CF ; code segment 0x08
dw 0xFFFF, 0x0000, 0x9200, 0x00CF ; data segment 0x10
gdt_end:
Load it with lgdt [gdt_desc] before switching to protected mode.
- The protected mode jump
After setting the PE bit in cr0, you must perform a far jump to reload CS with the new code segment selector. I initially forgot the jmp and the CPU continued with the old real‑mode CS, causing a triple fault.
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x08:protected_mode ; 0x08 is our code segment selector
- Segment registers in protected mode
Inside the kernel (or later, in interrupt handlers), I naively loaded ds = 0x1000 and es = 0xB800, assuming they would work like in real mode. That’s wrong – in protected mode they must hold selectors (indices into GDT). This caused instant #GP.
Fix: Use the data segment selector (0x10) and access video memory directly by its linear address:
mov ax, 0x10 ; data selector
mov ds, ax
mov es, ax
; Write character via linear address 0xB8000
mov edi, [cursor_pos]
shl edi, 1
add edi, 0xB8000
mov ah, 0x07
mov [edi], ax
Result: a working bootloader + kernel
After fixing these issues, the bootloader loads the kernel, enters protected mode, and runs a simple command line. Everything is open source.
GitHub repository: github.com/Paullknya/AXonOS-kernel
Project discussions and updates: paullknya.github.io
If you’re also writing a hobby OS, I hope this saves you a few hours of debugging. Feel free to ask questions in the discussions – I’ll be happy to help.
Happy coding!
my OS link: https://github.com/Paullknya/AXonOS-kernel
Top comments (0)