DEV Community

Paull Knya-z
Paull Knya-z

Posted on

Writing a Bootloader for My OS: Common Pitfalls and How I Fixed Them

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
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode

Then restore or use the saved value when reading sectors.

  1. 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
Enter fullscreen mode Exit fullscreen mode

Many emulators enable A20 by default, but real hardware doesn’t – always include this step.

  1. 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:
Enter fullscreen mode Exit fullscreen mode

Load it with lgdt [gdt_desc] before switching to protected mode.

  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode

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)