79 lines
3.9 KiB
Markdown
79 lines
3.9 KiB
Markdown
# 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.
|