Teaching It to Fail Gracefully
Milestone 4 of my from-scratch Rust kernel: the IDT and exception handling — where the machine stops silently rebooting on a mistake and starts telling me what went wrong. The whole thing lives or dies on assembly and Rust agreeing about the shape of the stack.
Contents
Milestone 4 — the GDT/IDT and exception handling. Where the kernel stops silently rebooting on a mistake and starts telling you what went wrong.
Before writing a line of code, I pinned down what this milestone was actually for: understanding how a CPU hands control to the kernel when something interrupts normal execution — the IDT and the interrupt calling convention. “Done” had to be observable behavior, not vibes: the kernel deliberately triggers a breakpoint (int3), prints that it caught it, and keeps running. And an unhandled fault prints a named report instead of rebooting.
The smallest version that still teaches the lesson: an IDT wired for all 256 vectors, but only a handful of handlers with real logic — breakpoint, page fault, and a generic fatal reporter. The int3 self-test doubles as the guard against accidentally working: if any link in the chain — table layout, stub, dispatch, iretq — is wrong, the machine faults or hangs instead of printing “survived the breakpoint.” And I named the thing most likely to stall me for days up front: a mismatch between the register order the assembly stub pushes and the Rust struct that reads it. Get that wrong and every field is silently wrong, with nothing to tell you so. Naming it meant I checked it explicitly.
The scoping decision
Interrupts are foundational, not creep — every later milestone (keyboard, timer, preemption) depends on them. But I made one deliberate reduction: skip the TSS/IST double-fault stack for now. It’s the “robust” answer, but it adds a second fiddly descriptor format to a milestone whose lesson is the IDT. Deferred, with the ist field already stubbed for when it comes.
The design
Approach. A 256-entry IDT built in Rust (src/interrupts.rs). Per-vector entry stubs generated in assembly (boot/isr.asm), because stable Rust can’t express the iretq entry/exit shape. Stubs normalise every fault into one InterruptContext layout and call one Rust dispatcher.
Machine-state preconditions. Runs in 64-bit long mode with the boot GDT still loaded (handlers use its code selector 0x08). Interrupts stay disabled the whole milestone — we never sti — so only synchronous exceptions can fire. That’s deliberate: no hardware IRQ can surprise us until the keyboard milestone.
Edge cases named. Error-code vs. no-error-code vectors (uniformised with a dummy push); the split-across-three-fields handler address in an IDT entry; 16-byte stack alignment at the call into Rust; the page-fault address living in CR2, not on the stack.
What got built
boot/isr.asm— a nasm macro emits all 256 stubs plus a sharedisr_commontrampoline and anisr_stub_tablethe Rust reads. The error-code-bearing vectors (8, 10–14, 17, 21, 29, 30) are handled distinctly from the rest.src/interrupts.rs— theIdtEntry/Idttypes,init()that fills andlidts the table, andinterrupt_dispatch, which decodes the vector:#BPreports and resumes;#PFreports CR2 + a decoded error code and halts; everything else reports and halts legibly.kernel_mainnow callsinterrupts::init()and fires theint3self-test.
What I verified — and what I didn’t
Assembles, compiles, and links cleanly — valid Multiboot2 image, no undefined symbols, no warnings. The interactive behavior (breakpoint recovery, page-fault report) is exercised by CI’s headless QEMU boot, not yet on real hardware. When it is run and something inevitably misbehaves, the debugging story goes here — that’s the honest, interesting part, and it’s not invented in advance.
What the next person should take away
The subtle, transferable idea isn’t “call lidt.” It’s that an interrupt is a contract written in two languages: assembly agrees to leave the machine state in a precise shape on the stack, and Rust agrees to read exactly that shape. The whole milestone lives or dies on those two descriptions matching. Everything else is bit-packing you can look up.
Newsletter
Liked this? Get the next one.
One essay or short note every other week — privacy-first software, AI, security, and the occasional dispatch from the trail. No filler.
More writing
Setting Up to Build Nothing
I'm building an x86-64 operating system from scratch in Rust, and this is milestone zero: a toolchain and an empty kernel that compiles for a machine that isn't my machine. No boot, no output — just the unglamorous groundwork where most OS projects quietly die.
ReadWhen the Machine Always Agrees: AI, Sycophancy, and Your Head
We learned the hard way what an engagement-optimized feed does to a mind. A model trained to agree with you is a different version of the same problem — and for some people it has already turned dangerous. How to use AI without letting it become a yes-man for your worst spirals.
ReadSteelmanning the Skeptics: The Strongest Case Against the Thing I Love
I'm optimistic about AI and use it every day — which is exactly why I wanted to write the best possible case against it. Not strawmen: the arguments that actually keep me up, argued as well as I can argue them.
Read