danos/src/arch/x86_64/paging.zig

71 lines
2.9 KiB
Zig

//! The kernel's own 4-level page tables. Until now we've been running on the
//! firmware's page tables, which live in memory we'd like to reclaim and which we
//! don't control. This builds our own set, identity-mapping the low 4 GiB, and
//! loads CR3 to switch onto them.
//!
//! "Identity map" means virtual address == physical address, which keeps
//! everything already running — kernel image, stack, framebuffer, the frame
//! allocator's bitmap, MMIO — valid across the switch without having to relocate
//! anything. 4 GiB comfortably covers all of that (RAM low down, the framebuffer
//! at 2 GiB, device MMIO below 4 GiB). Higher-half mapping and per-region
//! permissions come later; this is the bootstrap.
//!
//! We use 2 MiB pages, so the whole map is cheap: a PML4, a PDPT, and four page
//! directories.
const KiB = 1024;
const MiB = 1024 * KiB;
const GiB = 1024 * MiB;
const present: u64 = 1 << 0;
const writable: u64 = 1 << 1;
const huge: u64 = 1 << 7; // in a PD entry: this maps a 2 MiB page directly
const addr_mask: u64 = 0x000F_FFFF_FFFF_F000; // physical address bits of an entry
/// A page table is 512 64-bit entries. While building the tables the firmware's
/// identity map is still active, so a physical frame address is usable directly.
fn tableAt(phys: u64) *[512]u64 {
return @ptrFromInt(phys);
}
/// Allocate and zero a fresh page-table frame. Zeroing matters: the frame comes
/// from previously-used memory, and any stale non-zero entry would map a bogus
/// region.
fn allocTable(allocFrame: *const fn () ?u64) u64 {
const frame = allocFrame() orelse @panic("paging: out of memory building page tables");
@memset(tableAt(frame)[0..], 0);
return frame;
}
/// Return the table an entry points at, creating it if the entry is empty.
fn descend(entry: *u64, allocFrame: *const fn () ?u64) u64 {
if (entry.* & present != 0) return entry.* & addr_mask;
const frame = allocTable(allocFrame);
entry.* = frame | present | writable;
return frame;
}
/// Identity-map one 2 MiB page: walk PML4 -> PDPT -> PD and write the leaf.
fn mapHugePage(pml4: u64, addr: u64, allocFrame: *const fn () ?u64) void {
const pml4e = &tableAt(pml4)[(addr >> 39) & 0x1FF];
const pdpt = descend(pml4e, allocFrame);
const pdpte = &tableAt(pdpt)[(addr >> 30) & 0x1FF];
const pd = descend(pdpte, allocFrame);
tableAt(pd)[(addr >> 21) & 0x1FF] = addr | present | writable | huge;
}
/// Build the tables, identity-map the low 4 GiB, and switch CR3 onto them.
pub fn init(allocFrame: *const fn () ?u64) void {
const pml4 = allocTable(allocFrame);
var addr: u64 = 0;
while (addr < 4 * GiB) : (addr += 2 * MiB) {
mapHugePage(pml4, addr, allocFrame);
}
// Loading CR3 switches address spaces and flushes the TLB in one step.
asm volatile ("mov %[pml4], %%cr3"
:
: [pml4] "r" (pml4),
: .{ .memory = true }
);
}