413 lines
19 KiB
Zig
413 lines
19 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.
|
|
//!
|
|
//! Everything is 4 KiB pages — precise and simple; the extra table memory is
|
|
//! negligible against available RAM.
|
|
|
|
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 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;
|
|
|
|
// 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) 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 [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.
|
|
fn mapRangePhysmap(pml4: u64, physical_base: u64, len: u64, flags: u64) void {
|
|
var address = physical_base & ~@as(u64, page_size - 1);
|
|
const end = physical_base + len;
|
|
while (address < end) : (address += page_size) {
|
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, 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));
|
|
}
|
|
|
|
/// 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();
|
|
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);
|
|
}
|
|
|
|
// 2. Physmap windows for the framebuffer and the Local APIC (device memory
|
|
// the kernel touches directly), RW + NX.
|
|
const fb = boot_information.framebuffer;
|
|
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute);
|
|
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) void {
|
|
const flags: u64 = present | user | writable | no_execute | pcd | pwt | device_grant;
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// 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 (all danos mappings are 4 KiB, so no huge-page
|
|
/// case). 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;
|
|
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. All danos mappings are 4 KiB, so there is no huge-page
|
|
/// case. 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;
|
|
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 });
|
|
}
|