# Paging: the kernel's page tables and VMM 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. danos builds its own tables (rather than staying on the firmware's, which live in memory we'd like to reclaim and don't control), switches CR3 onto them, and — crucially — maps with **real permissions**. 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`. ## The format x86_64 uses **4 levels**: PML4 → PDPT → PD → PT, each a 512-entry table, with 9 bits of the virtual address indexing each level and the low 12 bits the offset into the final 4 KiB page. Each entry holds a physical address plus flag bits — present, writable, and (bit 63) **no-execute**. danos maps everything with 4 KiB pages: precise, and the extra table memory is negligible against available RAM. ## What gets mapped, and with what permissions The address space is built in three passes (`init`): 1. **All RAM, identity-mapped RW + NX.** Every non-MMIO region from the [memory map](memory-map.md) is mapped virtual == physical, read-write and *non-executable*. Identity mapping keeps everything already running valid across the CR3 switch (the frame allocator addresses frames by physical address, page tables are reached the same way, the stack stays put). 2. **The framebuffer and the Local APIC**, the device memory we actually touch, also RW + NX. Everything else — unbacked address space, other MMIO — is simply left unmapped, so a stray access faults instead of silently succeeding. 3. **The kernel's own segments, overlaid with their true ELF permissions.** This is the interesting part. ### W^X from the ELF program headers Blanket RW+NX is fine for data but wrong for the kernel's own code, which must be executable — and its code must *not* be writable (W^X: no page is both). We get the right permissions per region straight from the kernel ELF: the **loader already parses the program headers**, so `efi.zig` records each `PT_LOAD` segment's address, size and R/W/X flags into `BootInfo`. Pass 3 re-maps those ranges with flags derived from the ELF flags: | segment | ELF flags | mapped as | |---------|-----------|-----------| | `.text` | R + X | present, **not** writable, **not** NX | | `.rodata` | R | present, not writable, NX | | `.data`/`.bss` | R + W | present, writable, NX | So code can execute but not be written, and data can be written but not executed. (Intermediate table entries are left writable and executable so the *leaf's* bits govern — a page is writable only if every level is, and non-executable if any level is.) NX itself has to be switched on first via `EFER.NXE`, or the NX bit would be a reserved bit and fault. ### The null guard Page 0 is deliberately left unmapped. A null (or near-null) pointer dereference now takes a page fault instead of quietly reading or writing real memory — turning a whole class of silent bugs into an immediate, located crash. ## Switching on, and the on-demand API Loading the PML4's physical address into **CR3** switches address spaces and flushes the TLB in one step. This works because the firmware's identity map is still active *while we build*, so freshly allocated table frames are reachable by physical address; afterwards they're covered by pass 1. `init` keeps the PML4 and the frame allocator around and exposes `map(virt, phys, writable)` / `unmap(virt)` (with `invlpg` TLB invalidation) — the primitive the kernel heap will build on to map pages on demand. ## Verifying it Four tests (see [testing.md](testing.md)) pin down the guarantees: - **`vmm`** — map a fresh frame at an unused virtual address, write and read it back. Proves `map` works end to end. - **`fault-pf`** — an access far above all mapped RAM faults, with the address in CR2. Proves we're on our own (deliberately sparse) map. - **`fault-nx`** — calling into a data page (NX) faults on the instruction fetch. Proves NX is enforced. - **`fault-null`** — writing to address 0 faults. Proves the null guard. > Toolchain notes, both hit while writing the tests: a volatile access to a > compile-time-*constant* address either trips the self-hosted backend's > `mov moffs` gap or (for address 0) Zig's null-pointer safety check — so the > null-guard test launders the address through empty asm and uses an `allowzero` > pointer to force a real hardware access. And `invlpg`, like `lgdt`, needs its > operand staged through a register in inline asm. ## What's next (not done here) - **A kernel heap** — the first real user of `map`, giving the kernel dynamic allocation. This is the natural next milestone. - **A higher-half kernel**: relink the kernel at a high virtual base so a future user address space can own the low half. - **Per-address-space tables** once there are user processes, and shared/copy-on- write mappings. - **Uncacheable MMIO**: the APIC/framebuffer pages are mapped writeback-cacheable; real hardware wants MMIO marked uncacheable.