c + asm · x86-64 · qemu · mit

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.

qemu-system-x86_64 · COM1
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> _
inside the boot

from bios to init.

src/kernel/main.c — entry point
// 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();
}
what's in the box

four subsystems, no magic.

0x01

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.

0x02

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.

0x03

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.

0x04

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 bother

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.

try it

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.