# Paging: the kernel's own page tables Every memory access the CPU makes goes through the **page tables**: hardware walks them to translate a virtual address into a physical one, and faults if there's no valid mapping. Up to now danos ran on the *firmware's* page tables — which live in memory we'd like to reclaim and which we don't control. This step builds the kernel's own tables and switches onto them. It's x86_64-specific (the 4-level table format is an Intel/AMD thing), so it lives behind the [arch](arch.md) boundary in `src/arch/x86_64/paging.zig`. ## What we map, and why identity x86_64 uses **4 levels**: PML4 → PDPT → PD → PT, each a 512-entry table, with 9 bits of the virtual address indexing each level. A leaf can be a 4 KiB page (at the PT level) or, with the "huge" bit, a 2 MiB page straight from the PD. For this first set of tables we **identity-map the low 4 GiB** — virtual address equals physical address. Identity mapping is the pragmatic bootstrap: it keeps everything that's already running valid across the CR3 switch without relocating a single thing. The kernel image (at 1 MiB), the stack, the frame allocator's bitmap, the framebuffer (at 2 GiB), and device MMIO all sit in the low 4 GiB, so one flat identity range covers the lot. Using 2 MiB pages makes the whole map tiny: 4 GiB ÷ 2 MiB = 2048 leaves, which is one PML4, one PDPT, and four page directories — six frames total, from the [frame allocator](frame-allocator.md). ## Building and switching `paging.init` takes a frame allocator and: 1. Allocates and zeroes a PML4. **Zeroing matters**: the frame is recycled memory, and any stale non-zero entry would map a bogus region. 2. Walks PML4 → PDPT → PD for each 2 MiB address, creating intermediate tables on demand (`descend`) and writing the leaf with present + writable + huge. 3. Loads the PML4's physical address into **CR3**, which both switches address spaces and flushes the TLB in one instruction. This all works because the firmware's identity map is still active *while we build*, so a freshly allocated frame's physical address is usable directly as a pointer. After the CR3 load we're on our own tables — and since those page-table frames came from low RAM, they remain mapped (and thus editable) for later. ## Verifying it Two temporary tests confirmed both that we switched tables and that faults are caught with the right detail: - **Touch an address above 4 GiB** (`0xdeadbeef000`). Under the firmware's tables this *didn't* fault (they map a huge range); under ours it does: ``` CPU EXCEPTION: page fault (vector 14) error code : 0x2 (write, page not present) CR2 (addr) : 0x00000deadbeef000 (the faulting address) ``` A #PF at exactly the unmapped address is proof we're on our own, deliberately smaller, map — and that CR2 reporting works (see [interrupts.md](interrupts.md)). - The console keeps working *after* the switch, confirming the framebuffer, kernel code, and stack are all still mapped. > Toolchain note: a volatile store to a *compile-time-constant* address tripped a > codegen bug in the Zig self-hosted x86_64 backend ("no encoding for mov moffs"). > Computing the address in a runtime variable sidesteps it (register-relative > store). Worth remembering when poking fixed MMIO addresses. ## What's next (not done here) - **A higher-half kernel**: map the kernel at a high virtual base (e.g. `0xffffffff80000000`) so user address space can own the low half later. - **Real permissions**: today every page is writable and executable. Map code read-execute, data read-write + no-execute (needs setting EFER.NXE and the NX bit), and leave a guard page unmapped to catch null-ish dereferences. - **4 KiB pages / a general `map(virt, phys, flags)`** for fine-grained mappings, and unmapping with TLB invalidation (`invlpg`). - **Per-address-space tables** once there are user processes.