M2 step 5: drop the low half — a true higher-half kernel
paging.init now maps only the physmap, the framebuffer/LAPIC windows, and the kernel's own segments; the entire low canonical half is left to user space. Every higher-half PML4 entry is pre-created so a per-process address space can share the kernel half by copying PML4[256..512), with an assert against late top-half entries and a 4 GiB guard on pre-switch table frames. The AP trampoline's low identity page is now created transiently by arm() and unmapped by disarm(); startAp asserts the page-table root is 32-bit addressable. Docs (paging.md) updated. Suite 27/27; 4-core normal boot reaches /sbin/init. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f57a73e8a1
commit
bb7597ea0b
@@ -30,6 +30,23 @@ const pf_w: u32 = 2;
|
||||
var kernel_pml4: u64 = 0;
|
||||
var alloc_frame: *const fn () ?u64 = undefined;
|
||||
|
||||
/// 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
|
||||
@@ -42,6 +59,8 @@ fn tableAt(phys: u64) *[512]u64 {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -59,6 +78,11 @@ fn descend(entry: *u64) u64 {
|
||||
/// Map one 4 KiB page `virt` -> `phys` with `flags` (present is added).
|
||||
fn mapPage(pml4: u64, virt: u64, phys: u64, flags: u64) void {
|
||||
const pml4e = &tableAt(pml4)[(virt >> 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 (virt >> 63) == 1 and pml4e.* & present == 0)
|
||||
@panic("paging: new higher-half PML4 entry after init");
|
||||
const pdpt = descend(pml4e);
|
||||
const pdpte = &tableAt(pdpt)[(virt >> 30) & 0x1FF];
|
||||
const pd = descend(pdpte);
|
||||
@@ -67,16 +91,6 @@ fn mapPage(pml4: u64, virt: u64, phys: u64, flags: u64) void {
|
||||
tableAt(pt)[(virt >> 12) & 0x1FF] = (phys & addr_mask) | flags | present;
|
||||
}
|
||||
|
||||
/// Identity-map [base, base+len) with `flags`, rounded out to whole pages.
|
||||
fn mapRangeIdentity(pml4: u64, base: u64, len: u64, flags: u64) void {
|
||||
var addr = base & ~@as(u64, page_size - 1);
|
||||
const end = base + len;
|
||||
while (addr < end) : (addr += page_size) {
|
||||
if (addr == 0) continue; // leave page 0 unmapped: the null guard
|
||||
mapPage(pml4, addr, addr, flags);
|
||||
}
|
||||
}
|
||||
|
||||
/// Map [phys_base, phys_base+len) into the physmap (at physToVirt(phys)) 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.
|
||||
@@ -105,27 +119,23 @@ pub fn init(allocFrame: *const fn () ?u64, boot_info: *const danos.BootInfo) voi
|
||||
enableNx();
|
||||
const pml4 = allocTable();
|
||||
|
||||
// 1. All RAM in the physmap (physToVirt(phys)) RW + NX, plus — during the
|
||||
// higher-half transition — a low identity map so any not-yet-converted
|
||||
// physical deref still resolves. Non-RAM (MMIO) is skipped here and
|
||||
// mapped explicitly below. The identity half is removed in a later step.
|
||||
// 1. All RAM in the physmap (physToVirt(phys)) 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_info.memory_map)) |r| {
|
||||
if (r.kind == .mmio) continue;
|
||||
mapRangePhysmap(pml4, r.base, r.pages * page_size, present | writable | no_execute);
|
||||
mapRangeIdentity(pml4, r.base, r.pages * page_size, present | writable | no_execute);
|
||||
}
|
||||
|
||||
// 2. The framebuffer and the Local APIC (device memory we need), RW + NX —
|
||||
// in the physmap and (transitionally) identity.
|
||||
// 2. Physmap windows for the framebuffer and the Local APIC (device memory
|
||||
// the kernel touches directly), RW + NX.
|
||||
const fb = boot_info.framebuffer;
|
||||
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute);
|
||||
mapRangeIdentity(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute);
|
||||
mapPage(pml4, danos.physToVirt(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
||||
mapPage(pml4, 0xFEE00000, 0xFEE00000, present | writable | no_execute);
|
||||
|
||||
// 3. Overlay the kernel's own segments with their real ELF permissions,
|
||||
// replacing the blanket RW+NX from step 1: code becomes R+X, rodata R,
|
||||
// data R+W+NX. This is the W^X guarantee.
|
||||
// 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_info.kernel_segments[0..boot_info.kernel_segment_count]) |seg| {
|
||||
var flags: u64 = present;
|
||||
if (seg.flags & pf_w != 0) flags |= writable;
|
||||
@@ -136,12 +146,24 @@ pub fn init(allocFrame: *const fn () ?u64, boot_info: *const danos.BootInfo) voi
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/// Map a page into the kernel address space on demand (for the heap, etc.).
|
||||
|
||||
@@ -118,6 +118,9 @@ fn param(comptime name: []const u8) *align(1) volatile u64 {
|
||||
/// the running system). `cr3` is the kernel page tables the AP adopts. Precondition:
|
||||
/// `setTrampolinePage` has run.
|
||||
pub fn startAp(apic_id: u32, stack_top: usize, percpu: usize, index: usize, cr3: u64) bool {
|
||||
// The trampoline loads CR3 with a 32-bit `movl` before it reaches long mode,
|
||||
// so the page-table root must be addressable in 32 bits.
|
||||
if (cr3 >= (1 << 32)) @panic("smp: kernel page tables above 4 GiB");
|
||||
arm();
|
||||
defer disarm();
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ fn smoke(boot_info: *const BootInfo) void {
|
||||
|
||||
// The memory map has some usable RAM.
|
||||
const mm = boot_info.memory_map;
|
||||
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
|
||||
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(danos.physToVirt(mm.regions)))[0..mm.len];
|
||||
var usable: u64 = 0;
|
||||
for (regions) |r| {
|
||||
if (r.kind == .usable) usable += r.pages;
|
||||
|
||||
Reference in New Issue
Block a user