513 lines
26 KiB
Zig
513 lines
26 KiB
Zig
//! The kernel's page tables and virtual memory manager.
|
|
//!
|
|
//! Builds our own 4-level page tables and switches CR3 onto them, replacing the
|
|
//! firmware's. Unlike the earlier bootstrap this maps with real permissions:
|
|
//! RAM is identity-mapped read-write + no-execute, the kernel's own segments get
|
|
//! their ELF permissions (code R+X, rodata R, data R+W+NX), and page 0 is left
|
|
//! unmapped as a null guard. It also exposes map/unmap for on-demand mapping,
|
|
//! which the kernel heap will build on.
|
|
//!
|
|
//! The physmap (the permanent window onto all physical RAM) is built with 2 MiB
|
|
//! huge pages wherever the range is 2 MiB-aligned, falling back to 4 KiB for the
|
|
//! unaligned edges. On a big machine that is the difference between ~16.7M page-
|
|
//! table entries (128 MiB of tables) and ~32K — it makes both the build and the
|
|
//! footprint scale sanely with RAM. Everything else (kernel segments, heap, user
|
|
//! space, on-demand MMIO) stays 4 KiB: precise, and the table memory is
|
|
//! negligible there.
|
|
|
|
const boot_handoff = @import("boot-handoff");
|
|
const abi = @import("abi");
|
|
const io = @import("io.zig");
|
|
|
|
const page_size = abi.page_size;
|
|
|
|
// Page-table entry bits.
|
|
const present: u64 = 1 << 0;
|
|
const writable: u64 = 1 << 1;
|
|
const user: u64 = 1 << 2; // U/S: accessible from ring 3 (must be set at every level)
|
|
const pwt: u64 = 1 << 3; // page write-through
|
|
const pcd: u64 = 1 << 4; // page cache disable (with PWT: strong-uncacheable under the default PAT)
|
|
const page_size_bit: u64 = 1 << 7; // PS: this PDPT/PD entry is a 1 GiB/2 MiB leaf, not a pointer to the next table
|
|
const device_grant: u64 = 1 << 9; // available bit: this leaf maps device MMIO, not RAM — do not reclaim
|
|
const no_execute: u64 = 1 << 63;
|
|
const address_mask: u64 = 0x000F_FFFF_FFFF_F000;
|
|
|
|
// The PAT-index bit. In a 4 KiB PTE it is bit 7; in a huge leaf (2 MiB PDE / 1 GiB
|
|
// PDPTE) bit 7 is PS, so the PAT bit moves to bit 12. With PCD=PWT=0 this selects
|
|
// PAT entry 4, which `setupPat` programs to write-combining (see mapRangePhysmap).
|
|
const pte_pat: u64 = 1 << 7;
|
|
const huge_pat: u64 = 1 << 12;
|
|
const ia32_pat: u32 = 0x277;
|
|
|
|
/// The physmap's page size for 2 MiB-aligned RAM: one PD leaf covers this instead
|
|
/// of 512 PT entries. 4 KiB pages fill the unaligned edges (see mapRangePhysmap).
|
|
const huge_page_size: u64 = 2 << 20; // 2 MiB
|
|
|
|
// ELF segment flags (p_flags).
|
|
const pf_x: u32 = 1;
|
|
const pf_w: u32 = 2;
|
|
|
|
// State kept after init so map()/unmap() can serve later callers (e.g. the heap).
|
|
var kernel_pml4: u64 = 0;
|
|
var alloc_frame: *const fn () ?u64 = undefined;
|
|
var free_frame: *const fn (u64) void = undefined; // for tearing down address spaces
|
|
|
|
/// Set once the kernel is running on its own tables (past the CR3 load in
|
|
/// `init`). Before that, the kernel reaches page-table frames through the
|
|
/// *loader's* bootstrap physmap, which only covers the low 4 GiB — so every
|
|
/// frame allocated for a table during that window must be below 4 GiB. Both the
|
|
/// frame allocator and this code scan from low addresses up, so it holds
|
|
/// naturally; the assertion in `allocTable` makes a violation loud rather than
|
|
/// a silent fault. After the switch the kernel's own physmap covers all RAM.
|
|
var on_own_tables = false;
|
|
|
|
/// Set at the end of `init`. Guards against a new *higher-half* PML4 entry being
|
|
/// created afterward: the kernel half is pre-populated at init and then shared
|
|
/// by copying PML4[256..512) into every process address space (M3), so a late
|
|
/// top-half entry would be invisible to already-created address spaces.
|
|
var init_done = false;
|
|
|
|
const bootstrap_physmap_limit: u64 = 4 << 30;
|
|
|
|
/// Dereference a page-table frame by its physical address, via the physmap.
|
|
/// This is the single hinge for the higher-half move: page tables hold physical
|
|
/// frame addresses (pmm gives out physical frames, and CR3/PTEs must be
|
|
/// physical), but the kernel reaches them at `physmap_base + physical`. Valid under
|
|
/// both the loader's bootstrap tables and the kernel's own, which share the
|
|
/// physmap base.
|
|
fn tableAt(physical: u64) *[512]u64 {
|
|
return @ptrFromInt(boot_handoff.physicalToVirtual(physical));
|
|
}
|
|
|
|
fn allocTable() u64 {
|
|
const frame = alloc_frame() orelse @panic("paging: out of memory building page tables");
|
|
if (!on_own_tables and frame >= bootstrap_physmap_limit)
|
|
@panic("paging: table frame above the 4 GiB bootstrap physmap");
|
|
@memset(tableAt(frame)[0..], 0);
|
|
return frame;
|
|
}
|
|
|
|
/// Return the table an entry points at, creating it if empty. Intermediate
|
|
/// entries are writable and executable so the leaf's bits govern (a page is
|
|
/// writable only if every level is; non-executable if any level is).
|
|
fn descend(entry: *u64) u64 {
|
|
if (entry.* & present != 0) {
|
|
// A present-but-huge entry is a leaf, not a table: descending would read
|
|
// its 2 MiB/1 GiB data frame as a page table and corrupt RAM. This only
|
|
// fires on a bug — a 4 KiB map landing inside a physmap huge page — and a
|
|
// loud panic beats silent corruption. (The physmap and the 4 KiB regions
|
|
// live in disjoint PML4 slots, so it should never happen.)
|
|
if (entry.* & page_size_bit != 0) @panic("paging: descend through a huge-page leaf");
|
|
return entry.* & address_mask;
|
|
}
|
|
const frame = allocTable();
|
|
entry.* = frame | present | writable;
|
|
return frame;
|
|
}
|
|
|
|
/// Map one 4 KiB page `virtual` -> `physical` with `flags` (present is added).
|
|
fn mapPage(pml4: u64, virtual: u64, physical: u64, flags: u64) void {
|
|
const pml4e = &tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
|
// The kernel half is fixed after init: every top-half PML4 entry is
|
|
// pre-created so address spaces can share it by copying these slots. A new
|
|
// one here would be invisible to address spaces already made.
|
|
if (init_done and (virtual >> 63) == 1 and pml4e.* & present == 0)
|
|
@panic("paging: new higher-half PML4 entry after init");
|
|
const pdpt = descend(pml4e);
|
|
const pdpte = &tableAt(pdpt)[(virtual >> 30) & 0x1FF];
|
|
const pd = descend(pdpte);
|
|
const pde = &tableAt(pd)[(virtual >> 21) & 0x1FF];
|
|
const pt = descend(pde);
|
|
tableAt(pt)[(virtual >> 12) & 0x1FF] = (physical & address_mask) | flags | present;
|
|
}
|
|
|
|
/// Map one 2 MiB huge page `virtual` -> `physical` with `flags` — a leaf at the PD
|
|
/// level (PS bit set), with no PT beneath it. Both addresses must be 2 MiB-aligned.
|
|
/// One of these replaces 512 `mapPage`s (and the PT frame they'd need).
|
|
fn mapHugePage(pml4: u64, virtual: u64, physical: u64, flags: u64) void {
|
|
const pml4e = &tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
|
if (init_done and (virtual >> 63) == 1 and pml4e.* & present == 0)
|
|
@panic("paging: new higher-half PML4 entry after init");
|
|
const pdpt = descend(pml4e);
|
|
const pdpte = &tableAt(pdpt)[(virtual >> 30) & 0x1FF];
|
|
const pd = descend(pdpte);
|
|
tableAt(pd)[(virtual >> 21) & 0x1FF] = (physical & address_mask) | flags | present | page_size_bit;
|
|
}
|
|
|
|
/// Map [physical_base, physical_base+len) into the physmap (at physicalToVirtual(physical)) with
|
|
/// `flags`, rounded out to whole pages. This is how the kernel keeps a permanent
|
|
/// window onto physical memory once the low identity map goes away. The 2 MiB-
|
|
/// aligned interior is mapped with huge pages; the unaligned head/tail with 4 KiB.
|
|
/// `write_combining` selects the WC memory type (setupPat's PAT entry 4) via the
|
|
/// PAT bit — bit 7 in a 4 KiB PTE, bit 12 in a huge leaf — for the framebuffer.
|
|
fn mapRangePhysmap(pml4: u64, physical_base: u64, len: u64, flags: u64, write_combining: bool) void {
|
|
const pte_flags = if (write_combining) flags | pte_pat else flags;
|
|
const huge_flags = if (write_combining) flags | huge_pat else flags;
|
|
var address = physical_base & ~@as(u64, page_size - 1);
|
|
const end = physical_base + len;
|
|
// Head: 4 KiB pages up to the next 2 MiB boundary.
|
|
while (address < end and address & (huge_page_size - 1) != 0) : (address += page_size)
|
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, pte_flags);
|
|
// Interior: 2 MiB huge pages while a whole one still fits.
|
|
while (address + huge_page_size <= end) : (address += huge_page_size)
|
|
mapHugePage(pml4, boot_handoff.physicalToVirtual(address), address, huge_flags);
|
|
// Tail: 4 KiB pages for whatever is left.
|
|
while (address < end) : (address += page_size)
|
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, pte_flags);
|
|
}
|
|
|
|
fn regions(mm: boot_handoff.MemoryMap) []const boot_handoff.MemoryRegion {
|
|
return @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(mm.regions)))[0..mm.len];
|
|
}
|
|
|
|
/// Enable the NX bit in the page-table format (EFER.NXE). Must happen before we
|
|
/// load a CR3 whose entries set the NX bit, or those bits are reserved and fault.
|
|
fn enableNx() void {
|
|
const efer_msr = 0xC0000080;
|
|
io.wrmsr(efer_msr, io.rdmsr(efer_msr) | (1 << 11));
|
|
}
|
|
|
|
/// Program this core's PAT so entry 4 (selected by the PAT bit with PCD=PWT=0) is
|
|
/// **write-combining**, leaving the other seven at their reset types. Nothing else
|
|
/// in danos sets the PAT bit, so this changes no existing mapping — it only gives
|
|
/// the framebuffer a write-combining type, which turns its full-screen clear from
|
|
/// glacial (uncached writes to a GPU BAR, the real-hardware default via MTRRs) into
|
|
/// a batched burst. Must run on **every** core (PAT is per-logical-processor) — the
|
|
/// framebuffer mapping lives in the shared kernel half, so a core with the reset
|
|
/// PAT would see it as write-back and alias. Called from `init` (BSP) and each AP.
|
|
pub fn setupPat() void {
|
|
// Reset PAT is PA0=WB PA1=WT PA2=UC- PA3=UC PA4=WB PA5=WT PA6=UC- PA7=UC; flip
|
|
// PA4 from WB (0x06) to WC (0x01). Type codes: UC=0 WC=1 WT=4 WP=5 WB=6 UC-=7.
|
|
io.wrmsr(ia32_pat, 0x0007_0401_0007_0406);
|
|
}
|
|
|
|
/// Build the address space and switch onto it.
|
|
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
|
alloc_frame = allocFrame;
|
|
free_frame = freeFrame;
|
|
enableNx();
|
|
setupPat(); // BSP: PAT entry 4 = write-combining, for the framebuffer window
|
|
const pml4 = allocTable();
|
|
|
|
// 1. All RAM in the physmap (physicalToVirtual(physical)) RW + NX. No identity/low-half
|
|
// mapping: the low half belongs to user space. MMIO is skipped here and
|
|
// mapped on demand (mapMmio) or explicitly below.
|
|
for (regions(boot_information.memory_map)) |r| {
|
|
if (r.kind == .mmio) continue;
|
|
mapRangePhysmap(pml4, r.base, r.pages * page_size, present | writable | no_execute, false);
|
|
}
|
|
|
|
// 2. Physmap windows for the framebuffer and the Local APIC (device memory
|
|
// the kernel touches directly), RW + NX. The framebuffer is **write-
|
|
// combining** (see setupPat) so the console's full-screen clear is a burst,
|
|
// not millions of uncached single-word writes.
|
|
const fb = boot_information.framebuffer;
|
|
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute, true);
|
|
mapPage(pml4, boot_handoff.physicalToVirtual(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
|
|
|
// 3. The kernel's own segments at their higher-half link addresses, mapped
|
|
// to their low physical load addresses with real ELF permissions: code
|
|
// R+X, rodata R, data R+W+NX. This is the W^X guarantee.
|
|
for (boot_information.kernel_segments[0..boot_information.kernel_segment_count]) |seg| {
|
|
var flags: u64 = present;
|
|
if (seg.flags & pf_w != 0) flags |= writable;
|
|
if (seg.flags & pf_x == 0) flags |= no_execute;
|
|
var off: u64 = 0;
|
|
while (off < seg.pages * page_size) : (off += page_size) {
|
|
mapPage(pml4, seg.virtual + off, seg.physical + off, flags);
|
|
}
|
|
}
|
|
|
|
// 4. Pre-create every higher-half PML4 entry (an empty PDPT where none
|
|
// exists yet), so the whole kernel half is a fixed set of top-level
|
|
// slots. A process address space (M3) then shares the kernel half simply
|
|
// by copying PML4[256..512) — growth beneath these slots (heap, on-demand
|
|
// MMIO) propagates to every address space because they share the PDPTs.
|
|
for (256..512) |i| {
|
|
const e = &tableAt(pml4)[i];
|
|
if (e.* & present == 0) e.* = allocTable() | present | writable;
|
|
}
|
|
|
|
kernel_pml4 = pml4;
|
|
asm volatile ("mov %[pml4], %%cr3"
|
|
:
|
|
: [pml4] "r" (pml4),
|
|
: .{ .memory = true });
|
|
on_own_tables = true; // now on the kernel's physmap (covers all RAM)
|
|
init_done = true; // the kernel half is fixed from here
|
|
}
|
|
|
|
/// The kernel's own top-level page table (physical). Every kernel task and every
|
|
/// per-process address space shares this table's higher half.
|
|
pub fn kernelPml4() u64 {
|
|
return kernel_pml4;
|
|
}
|
|
|
|
/// Load CR3 (switch the active address space). `pml4` is a physical frame.
|
|
pub fn loadCr3(pml4: u64) void {
|
|
asm volatile ("mov %[pml4], %%cr3"
|
|
:
|
|
: [pml4] "r" (pml4),
|
|
: .{ .memory = true });
|
|
}
|
|
|
|
/// Map a page into the kernel address space on demand (for the heap, etc.).
|
|
/// `writable_page` controls W; pages are always mapped non-executable.
|
|
pub fn map(virtual: u64, physical: u64, writable_page: bool) void {
|
|
var flags: u64 = present | no_execute;
|
|
if (writable_page) flags |= writable;
|
|
mapPage(kernel_pml4, virtual, physical, flags);
|
|
invalidate(virtual);
|
|
}
|
|
|
|
/// Map a device MMIO range into the physmap and return the virtual address to
|
|
/// use for it (physicalToVirtual(physical)). The single way the kernel (and the device
|
|
/// layer, via the HAL) reaches memory-mapped registers once the identity map is
|
|
/// gone: physmap pages are RW + NX, so a driver never executes device memory.
|
|
/// Idempotent for already-mapped ranges. `len` 0 maps one page.
|
|
pub fn mapMmio(physical: u64, len: u64, writable_page: bool) u64 {
|
|
var flags: u64 = present | no_execute;
|
|
if (writable_page) flags |= writable;
|
|
const first = physical & ~@as(u64, page_size - 1);
|
|
const last = physical + (if (len == 0) 1 else len) - 1;
|
|
var address = first;
|
|
while (address <= (last & ~@as(u64, page_size - 1))) : (address += page_size) {
|
|
const virtual = boot_handoff.physicalToVirtual(address);
|
|
mapPage(kernel_pml4, virtual, address, flags);
|
|
invalidate(virtual);
|
|
}
|
|
return boot_handoff.physicalToVirtual(physical);
|
|
}
|
|
|
|
/// Like `descend`, but also sets the U/S bit on the intermediate entry (new or
|
|
/// pre-existing): ring-3 access requires U at *every* level, and `descend` leaves
|
|
/// existing entries untouched. Only used under user-exclusive virtual ranges, so
|
|
/// no kernel mapping's protection is widened (the leaf still governs).
|
|
fn descendUser(entry: *u64) u64 {
|
|
const table = descend(entry);
|
|
entry.* |= user;
|
|
return table;
|
|
}
|
|
|
|
/// Map one 4 KiB page `virtual` -> `physical` accessible from ring 3. W^X is the
|
|
/// caller's contract: code pages are read-only + executable, data pages are
|
|
/// writable + no-execute. `virtual` must lie in a user-exclusive region (see
|
|
/// `descendUser`).
|
|
pub fn mapUser(virtual: u64, physical: u64, writable_page: bool, executable: bool) void {
|
|
mapUserInto(kernel_pml4, virtual, physical, writable_page, executable);
|
|
}
|
|
|
|
/// Map a ring-3-accessible page into the address space rooted at `pml4` (which
|
|
/// may be a process's own table or the kernel's). W^X is the caller's contract.
|
|
pub fn mapUserInto(pml4: u64, virtual: u64, physical: u64, writable_page: bool, executable: bool) void {
|
|
var flags: u64 = present | user;
|
|
if (writable_page) flags |= writable;
|
|
if (!executable) flags |= no_execute;
|
|
const pml4e = &tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
|
const pdpt = descendUser(pml4e);
|
|
const pdpte = &tableAt(pdpt)[(virtual >> 30) & 0x1FF];
|
|
const pd = descendUser(pdpte);
|
|
const pde = &tableAt(pd)[(virtual >> 21) & 0x1FF];
|
|
const pt = descendUser(pde);
|
|
tableAt(pt)[(virtual >> 12) & 0x1FF] = (physical & address_mask) | flags;
|
|
invalidate(virtual);
|
|
}
|
|
|
|
/// Map a device MMIO window `[physical, physical+len)` into the user (low) half of the
|
|
/// address space rooted at `pml4`, page by page. Unlike `mapUserInto` these pages
|
|
/// are **strong-uncacheable** (PCD|PWT — device registers must not be cached) and
|
|
/// carry the `device_grant` bit so teardown does not return the MMIO frames to the
|
|
/// RAM allocator (`freeSubtree`). RW + NX; the caller places `virtual` in a
|
|
/// user-exclusive range (PML4[225]). Both `virtual` and `physical` are page-aligned by
|
|
/// the caller; a sub-page `physical` offset is the caller's to re-apply.
|
|
pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64, write_combining: bool) void {
|
|
// Registers are strong-uncacheable (PCD|PWT). A framebuffer instead wants
|
|
// write-combining — the PAT bit (bit 7 in a 4 KiB PTE) with PCD=PWT=0 selects PAT
|
|
// entry 4, which `setupPat` programs to WC — so pixel writes batch into bursts.
|
|
const cache: u64 = if (write_combining) pte_pat else (pcd | pwt);
|
|
const flags: u64 = present | user | writable | no_execute | device_grant | cache;
|
|
const first = physical & ~@as(u64, page_size - 1);
|
|
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
|
var off: u64 = 0;
|
|
while (first + off <= last) : (off += page_size) {
|
|
const v = virtual + off;
|
|
const pml4e = &tableAt(pml4)[(v >> 39) & 0x1FF];
|
|
const pdpt = descendUser(pml4e);
|
|
const pdpte = &tableAt(pdpt)[(v >> 30) & 0x1FF];
|
|
const pd = descendUser(pdpte);
|
|
const pde = &tableAt(pd)[(v >> 21) & 0x1FF];
|
|
const pt = descendUser(pde);
|
|
tableAt(pt)[(v >> 12) & 0x1FF] = ((first + off) & address_mask) | flags;
|
|
invalidate(v);
|
|
}
|
|
}
|
|
|
|
/// Map `[physical, physical+len)` into the user half rooted at `pml4` as **coherent
|
|
/// DMA memory**: strong-uncacheable (PCD|PWT — a device reads/writes this RAM without
|
|
/// snooping the CPU caches) but, unlike `mapUserDeviceInto`, **without** `device_grant`
|
|
/// — because these frames are real RAM from `pmm.allocContiguous`, so teardown
|
|
/// (`freeSubtree`) must return them to the allocator like any other user page. RW + NX.
|
|
/// The caller aligns `virtual`/`physical` and places `virtual` in the DMA arena.
|
|
pub fn mapUserDmaInto(pml4: u64, virtual: u64, physical: u64, len: u64) void {
|
|
const flags: u64 = present | user | writable | no_execute | pcd | pwt;
|
|
const first = physical & ~@as(u64, page_size - 1);
|
|
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
|
var off: u64 = 0;
|
|
while (first + off <= last) : (off += page_size) {
|
|
const v = virtual + off;
|
|
const pml4e = &tableAt(pml4)[(v >> 39) & 0x1FF];
|
|
const pdpt = descendUser(pml4e);
|
|
const pdpte = &tableAt(pdpt)[(v >> 30) & 0x1FF];
|
|
const pd = descendUser(pdpte);
|
|
const pde = &tableAt(pd)[(v >> 21) & 0x1FF];
|
|
const pt = descendUser(pde);
|
|
tableAt(pt)[(v >> 12) & 0x1FF] = ((first + off) & address_mask) | flags;
|
|
invalidate(v);
|
|
}
|
|
}
|
|
|
|
/// The raw leaf entry mapping `virtual` in the address space rooted at `pml4`, or null
|
|
/// if any level of the walk is absent. **Read-only** — never allocates or descends into
|
|
/// a missing table (unlike the `map*` paths' `descendUser`). Stops at the first huge
|
|
/// leaf. For tests and introspection that need a page's actual flag bits.
|
|
pub fn leafEntryOf(pml4: u64, virtual: u64) ?u64 {
|
|
const l4 = tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
|
if (l4 & present == 0) return null;
|
|
const l3 = tableAt(l4 & address_mask)[(virtual >> 30) & 0x1FF];
|
|
if (l3 & present == 0) return null;
|
|
if (l3 & page_size_bit != 0) return l3; // 1 GiB leaf
|
|
const l2 = tableAt(l3 & address_mask)[(virtual >> 21) & 0x1FF];
|
|
if (l2 & present == 0) return null;
|
|
if (l2 & page_size_bit != 0) return l2; // 2 MiB leaf
|
|
const l1 = tableAt(l2 & address_mask)[(virtual >> 12) & 0x1FF];
|
|
if (l1 & present == 0) return null;
|
|
return l1;
|
|
}
|
|
|
|
/// Is the 4 KiB leaf mapping `virtual` write-combining — the PAT bit set with PCD and
|
|
/// PWT clear, which `setupPat` makes PAT entry 4 (WC)? Null if unmapped. The device
|
|
/// mapping path (`mapUserDeviceInto`) always uses 4 KiB leaves, so bit 7 (`pte_pat`)
|
|
/// is the PAT selector in play.
|
|
pub fn leafIsWriteCombining(pml4: u64, virtual: u64) ?bool {
|
|
const e = leafEntryOf(pml4, virtual) orelse return null;
|
|
return (e & pte_pat != 0) and (e & pcd == 0) and (e & pwt == 0);
|
|
}
|
|
|
|
/// Create a new address space: a fresh PML4 with an empty user half and the
|
|
/// kernel's higher half shared in (copying PML4[256..512), whose entries point
|
|
/// at the kernel's PDPTs — pre-created at init and never restaled, so growth in
|
|
/// the kernel half propagates to every address space). Returns the physical
|
|
/// PML4, or null if out of frames.
|
|
pub fn createAddressSpace() ?u64 {
|
|
const pml4 = alloc_frame() orelse return null;
|
|
const t = tableAt(pml4);
|
|
@memset(t[0..256], 0); // empty user half
|
|
@memcpy(t[256..512], tableAt(kernel_pml4)[256..512]); // shared kernel half
|
|
return pml4;
|
|
}
|
|
|
|
/// Tear down an address space created by `createAddressSpace`: free every frame
|
|
/// and table in the user half [0..256), then the PML4 itself. The shared kernel
|
|
/// half [256..512) is never touched. The caller must not be running on `pml4`.
|
|
pub fn destroyAddressSpace(pml4: u64) void {
|
|
const t = tableAt(pml4);
|
|
for (0..256) |i| {
|
|
if (t[i] & present != 0) freeSubtree(t[i] & address_mask, 3); // PDPT level
|
|
}
|
|
free_frame(pml4);
|
|
}
|
|
|
|
/// Recursively free a page-table subtree: `level` 3 = PDPT, 2 = PD, 1 = PT. At
|
|
/// level 1 the entries are leaf data frames; above, they are child tables.
|
|
fn freeSubtree(physical: u64, level: u32) void {
|
|
const t = tableAt(physical);
|
|
for (t) |e| {
|
|
if (e & present == 0) continue;
|
|
if (level > 1) {
|
|
freeSubtree(e & address_mask, level - 1);
|
|
} else if (e & device_grant == 0) {
|
|
// A device-grant leaf points at MMIO, not RAM — returning it to the
|
|
// frame allocator would corrupt the pool. Only reclaim real RAM.
|
|
free_frame(e & address_mask);
|
|
}
|
|
}
|
|
free_frame(physical); // page-table frames are always real RAM
|
|
}
|
|
|
|
/// Whether `virtual` is currently mapped **executable** — present with the NX bit
|
|
/// clear. Walks the 4-level tables, stopping at a 2 MiB huge-page leaf (the physmap
|
|
/// uses them). Returns false if unmapped. Used for W^X checks in tests.
|
|
pub fn isExecutable(virtual: u64) bool {
|
|
const pml4e = tableAt(kernel_pml4)[(virtual >> 39) & 0x1FF];
|
|
if (pml4e & present == 0) return false;
|
|
const pdpte = tableAt(pml4e & address_mask)[(virtual >> 30) & 0x1FF];
|
|
if (pdpte & present == 0) return false;
|
|
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
|
|
if (pde & present == 0) return false;
|
|
if (pde & page_size_bit != 0) return pde & no_execute == 0; // 2 MiB huge leaf
|
|
const pte = tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF];
|
|
if (pte & present == 0) return false;
|
|
return pte & no_execute == 0;
|
|
}
|
|
|
|
/// Make an already-identity-mapped RAM page **executable** (clear its NX bit),
|
|
/// leaving it present and writable. The blanket RAM mapping is NX for W^X, but the
|
|
/// application processors fetch the AP trampoline from a low RAM page under paging —
|
|
/// so that one page must be executable. A deliberate, temporary W^X exception for a
|
|
/// single bring-up page; the caller frees it once every AP is up.
|
|
pub fn setExecutable(physical: u64) void {
|
|
mapPage(kernel_pml4, physical, physical, present | writable); // note: no no_execute
|
|
invalidate(physical);
|
|
}
|
|
|
|
/// Remove a mapping and flush it from the TLB.
|
|
pub fn unmap(virtual: u64) void {
|
|
unmapInto(kernel_pml4, virtual);
|
|
}
|
|
|
|
/// Remove a mapping from the address space rooted at `pml4` (a process's own
|
|
/// table or the kernel's) and flush it from the TLB. Clears only the leaf PTE —
|
|
/// the intermediate tables and any frame the PTE pointed at are left to the
|
|
/// caller (munmap frees the frame; `destroyAddressSpace` reclaims the tables).
|
|
pub fn unmapInto(pml4: u64, virtual: u64) void {
|
|
const pml4e = tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
|
if (pml4e & present == 0) return;
|
|
const pdpte = tableAt(pml4e & address_mask)[(virtual >> 30) & 0x1FF];
|
|
if (pdpte & present == 0) return;
|
|
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
|
|
if (pde & present == 0) return;
|
|
tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF] = 0;
|
|
invalidate(virtual);
|
|
}
|
|
|
|
/// Resolve a virtual address to a physical one in the address space rooted at
|
|
/// `pml4`, walking the tables through the physmap (CR3-independent — works for
|
|
/// any address space, not just the live one). Returns null if `virtual` is not
|
|
/// mapped at any level. Stops at a 2 MiB huge-page leaf (the physmap uses them),
|
|
/// resolving the offset within it. The foundation for cross-address-space copies
|
|
/// and for munmap (which needs the frame behind a user vaddr to free it).
|
|
pub fn translateIn(pml4: u64, virtual: u64) ?u64 {
|
|
const pml4e = tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
|
if (pml4e & present == 0) return null;
|
|
const pdpte = tableAt(pml4e & address_mask)[(virtual >> 30) & 0x1FF];
|
|
if (pdpte & present == 0) return null;
|
|
const pde = tableAt(pdpte & address_mask)[(virtual >> 21) & 0x1FF];
|
|
if (pde & present == 0) return null;
|
|
if (pde & page_size_bit != 0) // 2 MiB huge leaf: frame base is bits 51:21
|
|
return (pde & address_mask & ~@as(u64, huge_page_size - 1)) | (virtual & (huge_page_size - 1));
|
|
const pte = tableAt(pde & address_mask)[(virtual >> 12) & 0x1FF];
|
|
if (pte & present == 0) return null;
|
|
return (pte & address_mask) | (virtual & (page_size - 1));
|
|
}
|
|
|
|
fn invalidate(virtual: u64) void {
|
|
// invlpg needs its operand via a register-indirect memory reference that Zig
|
|
// inline asm won't form directly, so stage the address in a register first.
|
|
asm volatile (
|
|
\\mov %[v], %%rax
|
|
\\invlpg (%%rax)
|
|
:
|
|
: [v] "r" (virtual),
|
|
: .{ .rax = true, .memory = true });
|
|
}
|