//! 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 } ); }