Building My First Multiboot Toy Kernel (x86, C + Assembly)
TL;DR
I built a 32-bit freestanding toy kernel that:
- boots via GRUB using a valid Multiboot header
- jumps from assembly into C (
kernel_main) - writes directly to VGA text memory (
0xB8000) - sets up a minimal GDT (
null,code,data) - installs IDT entries for CPU exceptions (
0-31) - routes exceptions through common ISR stubs into a C panic path
This is not a full OS yet, but the bring-up foundation is solid.
Why I Started
I wanted to understand boot flow at a lower level than normal app dev: from bootloader handoff all the way to my own interrupt/exception handling path.
Target + Stack
- Architecture: x86 (32-bit protected mode)
- Bootloader: GRUB + Multiboot
- Language: C (freestanding) + NASM assembly
- Emulator: QEMU
Current Project Files
boot.s-> Multiboot header +_startlinker.ld-> load address + segment permissionsMakefile-> freestanding build + ISO pipelinekernel.c-> kernel entry, VGA output, panic, exception displaygdt.h,gdt.c,gdt.s-> GDT setup + flush/reload segmentsidt.h,idt.c-> IDT structs + gate installationisr.s-> exception ISR stubs (0-31)grub.cfg-> GRUB menu entry
Boot Pipeline
1) GRUB loads kernel
grub.cfg:
multiboot /boot/kernel.binboot
2) _start in assembly
In boot.s:
- disable interrupts (
cli) - initialize stack (
esp = stack_top) - pass Multiboot values from registers to C
- call
kernel_main
3) Freestanding kernel build
The Makefile uses kernel-safe flags:
-ffreestanding-m32- no PIE/PIC
- no stack protector
-nostdlib
Then builds kernel.bin and packs it into myos.iso using grub-mkrescue.
4) Runtime path in C
In kernel_main:
- validate multiboot magic
- print boot status to VGA
- initialize GDT
- initialize IDT
- trigger
int $0test - route through ISR common stub -> C exception handler -> panic
Subsystems Implemented
VGA output
Simple text writes to 0xB8000 for status and panic info.
GDT
Minimal 3-entry table:
- null descriptor
- kernel code segment (
0x08) - kernel data segment (
0x10)
gdt_flush:
lgdt- reload
ds/es/fs/gs/ss = 0x10 - far jump to reload
cs = 0x08
IDT
- zero init all 256 entries
- installs exception handlers for vectors
0..31 lidtthe table
ISR + Panic
isr.s common stub:
- saves registers/segments
- normalizes stack for handler
- calls
isr_handler(struct interrupt_frame*) - restores context and
iret
kernel.c handler:
- maps
int_noto exception names - prints exception context message
- enters panic halt loop
Debugging Milestones I Hit
Section placement issues
Early interrupt stub placement caused bad control flow. Keeping handlers in executable .text fixed this.
Linking/model issues
Hosted defaults caused bad kernel assumptions. Freestanding + linker cleanup made behavior predictable.
Triple-fault style resets
Bad selector/state setup caused reset-like behavior. Fixing selector usage first, then owning GDT state, stabilized execution.
Lack of observability
Before common ISR flow, faults were hard to reason about. Unified interrupt frame + panic output made faults visible.
Current Runtime Behavior
On boot:
- kernel status message prints
- multiboot handoff is verified
- GDT is initialized
- IDT is initialized
- test
int $0fires intentionally - exception path displays and halts cleanly
Timeline Highlights (Feb 15-24, 2026)
- initial boot skeleton + docs
- linker script + GRUB config
- first IDT wiring
- test ISR0 path
- build/link corrections
- multiboot handoff hardening
- full exception handling pipeline
- own GDT setup integrated
Screenshots
Next Work
- PIC remap (8259)
- IRQ handlers (timer, keyboard)
- better terminal layer (newlines/scroll)
- serial logging for headless debugging
- physical memory manager groundwork
Repro
cd /home/welsh/kernelmake cleanmakemake runMultiboot check:
grub-file --is-x86-multiboot kernel.bin && echo "multiboot ok"Next reads
View all →3 Sept
Building a Custom ELF Loader from Scratch, in C
You type ./program and the kernel loads it. Here's how that works: we build a userspace ELF loader in ~300 lines of C that maps PT_LOAD segments, builds a stack by hand, and jumps to the entry point.
9 Sept
Mini-Readelf: Gluing It All Together
The capstone. Four parts of pieces, headers, sections, symbols, relocations, joined into one tool that reads any ELF. The only new mechanic is the sh_link chain: offset into a table that holds offsets into a table that holds strings.
5 Sept
Relocations: How PIE Binaries Fix Their Addresses
A PIE binary can't write final addresses because ASLR moves it. The linker leaves placeholders and the loader patches them after mapping. That's a relocation: R_X86_64_RELATIVE, GLOB_DAT and JUMP_SLOT.
Get posts by email
One email when I publish, not a drip, not weekly. Sign up and I'll only write when there's something new.
You won't get mail just for signing up. Unsubscribe any time.