71 lines
2.9 KiB
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 }
|
|
);
|
|
}
|