Building an Operating System from Zero: What It Taught Me About Computers
There's no better way to understand how software works than to build the thing it runs on. Starting from a blank bootloader, I implemented interrupt handling, a keyboard driver, and memory paging — entirely from scratch, no frameworks underneath.
This kernel development guide walks through KFS-1 and KFS-2, grounded in OSDev fundamentals. We move from bare-metal bootstrapping to interrupts, drivers, and a debug shell, showing how each low-level concept becomes working code.
If this is your first kernel, you are in the right place. The goal is to make the boot path, memory layout, and interrupt model feel concrete, not mysterious.
What You Build (KFS-1 and KFS-2)
- Multiboot-compatible kernel that boots via GRUB.
- Custom linker script and freestanding toolchain.
- GDT and IDT setup with full ISR/IRQ plumbing.
- VGA text terminal with scrolling and multiple screens.
- Keyboard driver with scancode decoding.
- KFS-2: interactive shell with tab completion, GDT introspection, and stack inspection.
Fundamentals
Kernel development starts by understanding the real boot path and the minimum CPU structures that must be initialized before anything else works. These are the core concepts:
Boot and Tooling
- Cross-Compilation: Your host compiler targets your OS, not your kernel. Build a dedicated cross-compiler so binaries are produced for the target CPU/ABI without host OS assumptions.
- Bootstrapping: Power-on → BIOS/UEFI init → POST → boot device selection → bootloader → kernel load → system services. Your kernel lives at the end of this chain.
- Bootloader and GRUB: The bootloader loads your kernel into memory and jumps to it. GRUB is the common choice because it is Multiboot compliant and supports many filesystem types.
- Multiboot + Header: A magic number, flags, and checksum placed in the first 8 KiB lets GRUB recognize your kernel and pass control to it.
- Linker Script: Defines the kernel’s memory layout: where
.text,.data,.bss, and the stack live and how they align.
CPU and Interrupt Model
- GDT: The Global Descriptor Table defines segment privileges and boundaries. Even a flat memory model requires a valid GDT with a null descriptor and kernel segments.
- IDT: The Interrupt Descriptor Table maps interrupt vectors to handlers (ISRs) so the CPU knows how to react to exceptions and hardware events.
- ISRs: Interrupt Service Routines save CPU state and transfer control to C handlers.
- IRQ + PIC: Hardware devices signal interrupts through the PIC. The PIC must be remapped, and an End-of-Interrupt signal must be sent to complete servicing.
- Keyboard Controller: Scancodes are read from port
0x60, and status is checked via port0x64. IRQ1 signals new keyboard input.
Part 1: Tooling and Environment
Cross-Compiler and Freestanding Build
Kernel code cannot be built with your system gcc. A hosted compiler pulls in system libraries
and assumes an existing OS. Instead, you build a freestanding toolchain (for example i686-elf-gcc)
that targets raw hardware with no OS dependencies.
The core flags used in KFS are:
-ffreestanding -nostdlib -nodefaultlibs -fno-builtin -fno-stack-protector
These flags ensure the compiler emits standalone code and does not assume libc or a runtime.
Boot Image and Emulator
We build a bootable ISO using GRUB and run it in QEMU. This gives fast feedback without touching real hardware.
grub-mkrescue -o build/kernel.iso isodir
qemu-system-i386 -cdrom build/kernel.iso
Part 2: Bootstrapping and Multiboot
The OSDev "Bare Bones" guidance makes one point clear: your kernel is just a program, but it must obey the boot protocol. For Multiboot, that means a header in the first 8 KiB so GRUB can recognize it.
Multiboot Header (KFS-1)
section .multiboot
align 4
dd 0x1BADB002
dd 0x00
dd - (0x1BADB002 + 0x00)
Without this header, GRUB skips your kernel entirely.
Linker Script and Memory Layout
KFS places the kernel at 2MB and aligns each section to 4KB boundaries. This mirrors OSDev best practices and makes the transition to paging straightforward later.
ENTRY(_start)
SECTIONS {
. = 2M;
.text BLOCK(4K) : ALIGN(4K) { *(.multiboot) *(.text) }
.rodata BLOCK(4K) : ALIGN(4K) { *(.rodata) }
.data BLOCK(4K) : ALIGN(4K) { *(.data) }
.bss BLOCK(4K) : ALIGN(4K) { *(COMMON) *(.bss) }
}
Kernel Entry and Stack
The assembly entry sets a stack pointer, calls kernel_main, and halts if it ever returns.
_start:
mov $stack_top, %esp
call kernel_main
cli
1: hlt
jmp 1b
Recap: once GRUB loads the kernel and you control the stack, you can initialize CPU structures safely.
Part 3: First Output - VGA Terminal
OSDev fundamentals recommend starting in VGA text mode. KFS writes directly to 0xB8000
and builds a buffered terminal with scrolling and multiple screens.
- Each character cell is 2 bytes: ASCII + color attribute.
- Cursor updates via ports
0x3D4and0x3D5. - Buffered screens allow alt+arrow switching and scrollback.
Part 4: Segmentation and the GDT
Even with a flat memory model, the CPU requires a valid GDT. KFS-1 sets up 3 entries (null, kernel code, kernel data). KFS-2 expands to 7 entries, adding kernel stack and user segments.
gdt_set_gate(0, 0, 0, 0, 0);
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); // kernel code
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); // kernel data
gdt_set_gate(3, 0, 0xFFFFFFFF, 0x96, 0xCF); // kernel stack
gdt_set_gate(4, 0, 0xFFFFFFFF, 0xFA, 0xCF); // user code
gdt_set_gate(5, 0, 0xFFFFFFFF, 0xF2, 0xCF); // user data
gdt_set_gate(6, 0, 0xFFFFFFFF, 0xF6, 0xCF); // user stack
The assembly stub uses lgdt and reloads segment registers to finalize the switch.
Part 5: Interrupts - IDT, ISRs, and IRQs
IDT and Exceptions
The IDT defines 256 interrupt gates. The CPU uses vectors 0-31 for exceptions (divide-by-zero, page fault, etc.). KFS installs assembly stubs and routes into a C handler with a saved register frame.
- Each ISR saves registers with
pusha. - Segment registers are restored after the handler.
iretreturns to interrupted code.
PIC Remap and IRQs
Hardware interrupts from the PIC conflict with CPU exception vectors unless remapped. KFS remaps IRQs to 32-47, then sets IDT gates for each IRQ line.
outb(0x20, 0x11);
outb(0xA0, 0x11);
outb(0x21, 0x20);
outb(0xA1, 0x28);
outb(0x21, 0x04);
outb(0xA1, 0x02);
outb(0x21, 0x01);
outb(0xA1, 0x01);
outb(0x21, 0x00);
outb(0xA1, 0x00);
Each IRQ handler must send an EOI (0x20) to the PIC before returning.
Part 6: Keyboard Driver and Input Flow
The keyboard raises IRQ1. KFS reads scancodes from port 0x60 and converts them into ASCII using
a mapping table. Shift and Alt state are tracked, and extended scancodes are handled for arrows.
- User presses a key.
- Keyboard controller triggers IRQ1.
- CPU jumps to IDT entry 33 (IRQ1 remapped).
- Assembly ISR calls
keyboard_handler(). - Scancode is translated and printed to the terminal.
In KFS-1, arrow keys scroll and Alt+arrows switch screens. In KFS-2, ESC toggles the debug shell.
Part 7: KFS-2 Debug Shell
KFS-2 introduces a minimal shell to inspect kernel state. It parses input, supports tab completion, and dispatches commands via a function table.
- help: List available commands.
- gdt: Print GDT entries and access flags.
- stack: Inspect stack pointer and contents.
- time: Read RTC time via CMOS ports 0x70/0x71.
- reboot and halt: System control.
This makes the kernel interactive without a full userland.
Conclusion
KFS-1 and KFS-2 transform OSDev fundamentals into a working kernel: you boot with Multiboot, bring up a freestanding toolchain, wire the GDT and IDT, handle exceptions and hardware interrupts, and build a responsive terminal with real keyboard input. The debug shell in KFS-2 then turns your kernel into an interactive system, making low-level state visible and testable.
The core lesson is simple: an operating system is a program that owns the machine. Once you control the boot path, memory layout, CPU state, and interrupts, every higher-level feature becomes an engineering choice, not a mystery.
Written by

Technical Lead and Full Stack Engineer leading a 5-engineer team at Fygurs (Paris, Remote) on Azure cloud-native SaaS. Graduate of 1337 Coding School (42 Network / UM6P). Writes about architecture, cloud infrastructure, and engineering leadership.