a kernel that boots, schedules, and fits in my head.
a multitasking x86 kernel written in c and a little assembly. boots in qemu, runs real processes, switches between them every 10ms. protected mode, 4-level paging, vga text driver, a keyboard irq handler, a syscall table with five entries. i haven't counted in a while, somewhere around 3k lines. reading it should feel possible, not intimidating.
seabios (qemu) loading stage1.bin . . . . . . . . . . ok loading stage2.bin . . . . . . . . . . ok entering protected mode [ok] setting up gdt [ok] setting up idt + pic remap [ok] enabling paging (cr0 pg bit) [ok] initializing heap (1 mib) [ok] spawning init (pid 1) [ok] nanokern 0.3.0 · booted in 0.04s init> _
from bios to init.
// stage1 jumps here after long mode + higher-half remap void kmain(struct boot_info *bi) { vga_init(); // 80x25 text, cursor at (0,0) gdt_install(); // flat 64-bit segments + tss idt_install(); // 256 entries, pic remapped 0x20..0x2f pmm_init(bi->mmap, bi->mmap_len); // physical frame allocator vmm_init(); // pml4 for kernel, cr3 loaded kheap_init(1 << 20); // 1 MiB slab + bump pit_init(100); // 10ms tick kbd_init(); // ps/2 scancode set 1 sched_init(); task_spawn(init_main, "init"); // pid 1 sti(); // interrupts on, we're alive for (;;) hlt(); }
four subsystems, no magic.
stage1 + stage2 bootsector
512 bytes of real-mode asm that reads stage2 off disk, flips into protected mode, sets up a flat gdt, long-jumps into c. prints dots as it loads. i triple-faulted here for two days before realizing i had the gdt limit wrong.
4-level paging, x86-64
pml4 → pdpt → pd → pt. identity-mapped low 4 MiB for early boot, kernel remapped to the higher half at 0xffffffff80000000. cr3 reload, tlb flush, the works. turns out x86 is genuinely weird about tlb shootdowns.
preemptive scheduler + five syscalls
round-robin, 10ms quantum driven by the pit. context switch is 14 instructions of asm (saves callees, swaps rsp, iretq). ring 0 only for now. write, read, spawn, exit, sleep via int 0x80, sysret on the todo.
keyboard, vga, slab + bump
irq1 reads ps/2 scancode set 1, translates to ascii, drops it in a ring buffer. vga mmio at 0xb8000, 80×25, 16 colors. slab caches for task_t / vma_t / dentry_t. a dumb bump allocator for the 1 MiB init heap. kfree exists but lies about fragmentation.
why write a kernel when linux exists?
the jump from "i use an os" to "i understand what the cpu does before main()" is one of the largest in programming. every abstraction you take for granted (processes, files, scheduling, virtual memory) turns out to be a few hundred lines of c and an lgdt. until you write them, they feel like weather; after you write them, they feel like furniture. when stage1 finally prints a character to vga it's one of the purest feelings in programming: no framework, no runtime, no standard library, just a byte you put at 0xb8000 and the hardware obeying. i know this is a toy. that's the whole point. real kernels are too big to hold in my head at once. this one fits.
not public yet.
source drops on github soon, i'm still polishing the repo. you'll need gcc, nasm, and qemu to boot it once it ships. email bennett@frkhd.com if you want a preview.