From 21b9691486481e7f972b175442383cb79f5baa57 Mon Sep 17 00:00:00 2001 From: Daniel Samson Date: Fri, 3 Jul 2026 19:31:32 +0100 Subject: [PATCH] reclaiming uefi memory --- docs/frame-allocator.md | 23 ++++++++++++++---- docs/memory-map.md | 52 ++++++++++++++++++++++++----------------- src/efi.zig | 23 +++++++++++++----- src/main.zig | 9 +++---- src/pmm.zig | 9 +++---- src/root.zig | 11 +++++---- 6 files changed, 79 insertions(+), 48 deletions(-) diff --git a/docs/frame-allocator.md b/docs/frame-allocator.md index 8f6e2f9..fa81c33 100644 --- a/docs/frame-allocator.md +++ b/docs/frame-allocator.md @@ -100,12 +100,25 @@ The `free frames` MiB agreeing with the memory map's `usable RAM`, the three distinct consecutive addresses, and the count returning to its start after freeing are the three signals that init, alloc and free are all correct. +## Boot-services memory comes pre-reclaimed + +The UEFI boot-services memory (~44 MiB) is defunct and free once +`ExitBootServices` runs, taking usable RAM from ~76 MiB up to ~121 MiB. The frame +allocator does **nothing special** to get it: the loader already classified it as +`usable` (see [memory-map.md](memory-map.md)), so it's just part of the `usable` +regions `init` frees. Keeping that boot-protocol knowledge on the loader side is +deliberate — the kernel has no notion of "reclaimable" or of UEFI at all. + +The one live piece in that memory is the boot stack the kernel starts on; the loader +leaves the single region containing it `reserved`, so `init` won't hand it out. A +later step will move task 0 onto a kernel-owned stack, freeing that last ~1 MiB +region too (and giving user mode the clean stack it wants). + ## What's next (not done here) - **Contiguous allocation** — scan for N consecutive free bits — for callers that need physically adjacent frames. -- **Consumers**: the virtual memory manager / page tables and then the kernel heap - will be the first real users, each asking `alloc()` for frames. -- **Reclaiming `reclaimable`** (UEFI boot-services) memory, and eventually the - `reserved` `loader_data` (kernel image, boot buffers) once nothing needs it — - see the deferred list in [memory-map.md](memory-map.md). +- **A kernel stack for task 0**, so the boot stack's region can be freed too (and + for the clean stack user mode wants). +- **Freeing the `reserved` `loader_data`** (the boot-time map buffers) once the + kernel is done reading the memory map. diff --git a/docs/memory-map.md b/docs/memory-map.md index 967d6ae..ecc06a3 100644 --- a/docs/memory-map.md +++ b/docs/memory-map.md @@ -32,8 +32,7 @@ Defined in `src/root.zig`, the shared loader↔kernel contract: ```zig pub const MemoryKind = enum(u32) { usable, // free RAM the kernel may allocate - reserved, // firmware / kernel image — real RAM, but never hand out - reclaimable, // usable once boot-time structures are done with + reserved, // firmware / kernel image / boot stack — real RAM, never hand out acpi_tables, // parse, then reclaim acpi_nvs, // preserve across sleep mmio, // device registers / reserved address space — not RAM at all @@ -72,11 +71,19 @@ pub const BootInfo = extern struct { Two functions in `src/efi.zig`, called from `exitBootServices`: - **`classify`** maps each UEFI descriptor to a `MemoryKind`: - `conventional_memory → usable`; `boot_services_code`/`boot_services_data → - reclaimable` (free once we've exited); `acpi_reclaim_memory → acpi_tables`; - `acpi_memory_nvs → acpi_nvs`; **everything else → reserved** (the safe default). - Our own `loader_data` — the kernel image and these buffers — falls into - `reserved`, so it won't be handed out until the kernel deliberately reclaims it. + `conventional_memory` **and** `boot_services_code`/`boot_services_data → usable`; + `acpi_reclaim_memory → acpi_tables`; `acpi_memory_nvs → acpi_nvs`; **everything + else → reserved** (the safe default). Our own `loader_data` — the kernel image and + these buffers — falls into `reserved`. + + Folding boot-services memory into `usable` is deliberate: we've already called + ExitBootServices, so it's free RAM now, and doing the classification *here* (in + the loader) means the kernel never learns about a UEFI-specific "reclaimable" + state — it just sees usable RAM. The one catch is that our stack lives in + boot-services memory and the kernel starts out running on it, so + `convertMemoryMap` keeps the single region containing the current stack pointer + `reserved`. All the boot-protocol knowledge stays on the loader side of the + boundary; the kernel's frame allocator has no idea any of this happened. One subtlety: **a region that isn't writeback-cacheable (the descriptor's `wb` attribute) is classified `mmio` regardless of type.** UEFI overloads @@ -126,18 +133,17 @@ for (regions) |r| { ``` danos: physical memory total RAM : 0.12 GiB (127 MiB) - RAM the firmware reported - usable : 77 MiB - free now; owned by the frame allocator - reclaimable: 44 MiB - UEFI boot-services memory, free after exit - reserved : 6 MiB - kernel image, ACPI, runtime services - regions : 35 - entries in the firmware memory map + usable : 121 MiB - free RAM (incl. reclaimed boot-services memory) + reserved : 6 MiB - kernel image, boot stack, ACPI, runtime services + regions : 28 - entries in the firmware memory map ``` -The `usable` figure is only ~77 of ~127 MiB because most of the rest is -`reclaimable` boot-services memory — real RAM we'll take back once we implement -reclaiming, not memory that's gone. `total` counts only writeback-cacheable RAM, -so the ~12 GiB PCIe address hole is excluded (it's `mmio`), and the three RAM -categories summing back to the firmware's total is the sanity check that nothing -was dropped. +`usable` is ~121 of ~127 MiB because the loader already folded the boot-services +memory into it — so the frame allocator gets it all with no special step. The ~6 MiB +`reserved` is the kernel image, the boot stack's region, ACPI, and runtime services. +`total` counts only writeback-cacheable RAM, so the ~12 GiB PCIe address hole is +excluded (it's `mmio`), and the RAM categories summing back to the firmware's total +is the sanity check that nothing was dropped. ## How Raspberry Pi will fit @@ -150,11 +156,13 @@ never knows the difference. ## What's next This page is plumbing plus classification only. The map's first consumer, the -**physical frame allocator**, is built directly on the `usable` regions here — -see [frame-allocator.md](frame-allocator.md). Still to come after that: +**physical frame allocator**, is built directly on the `usable` regions here — which +already include the reclaimed boot-services memory the loader folded in (see +[frame-allocator.md](frame-allocator.md)). Still to come: -- Reclaiming `reclaimable` regions, and carefully freeing `reserved` `loader_data` - (kernel image, these buffers) once the kernel is done reading them. -- Paging / the kernel's own page tables, then a heap. +- Freeing the `reserved` `loader_data` (these boot-time buffers) once the kernel is + done reading the map. +- Capturing the ACPI RSDP from the UEFI configuration table before exit (the same + "grab it before ExitBootServices" pattern), for when ACPI parsing arrives. See the roadmap in [efi.md](efi.md) for where this sits in the boot flow. diff --git a/src/efi.zig b/src/efi.zig index e691f65..e889c39 100644 --- a/src/efi.zig +++ b/src/efi.zig @@ -269,6 +269,12 @@ fn exitBootServices(bs: *uefi.tables.BootServices) !danos.MemoryMap { /// never sees UEFI's vocabulary — the same seam the framebuffer already uses. fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap { const regions: [*]danos.MemoryRegion = @ptrCast(@alignCast(out.ptr)); + // We're about to call boot-services memory `usable`, but our own stack lives + // in it and the kernel starts out running on it. Keep the region holding the + // current stack pointer reserved so it's never handed out. + const rsp = asm volatile ("mov %%rsp, %[out]" + : [out] "=r" (-> usize), + ); var count: usize = 0; var i: usize = 0; while (i < map.info.len) : (i += 1) { @@ -277,7 +283,10 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap { const d: *const uefi.tables.MemoryDescriptor = @ptrCast(@alignCast(map.ptr + i * map.info.descriptor_size)); if (d.number_of_pages == 0) continue; - const kind = classify(d); + var kind = classify(d); + // The descriptor we're executing on stays reserved (see rsp above). + const region_end = d.physical_start + d.number_of_pages * danos.page_size; + if (kind == .usable and rsp >= d.physical_start and rsp < region_end) kind = .reserved; // Coalesce with the previous region if it's the same kind and contiguous. if (count > 0) { @@ -304,14 +313,16 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap { /// a reserved address-space window (e.g. PCIe config space) — so it's `mmio` /// regardless of type. UEFI overloads `reserved_memory_type` for both reserved RAM /// and such holes, and the cache attribute is what actually tells them apart. -/// Among RAM regions, anything we don't recognise is `reserved` — the safe -/// default; our own LoaderData (kernel image, these buffers) lands there too and -/// stays reserved until the kernel reclaims it. +/// +/// Boot-services memory is folded straight into `usable`: we've already called +/// ExitBootServices, so it's free RAM now — the kernel never needs to know it was +/// ever the firmware's (the one live piece, our stack, is reserved by the caller). +/// Anything unrecognised is `reserved` — the safe default; our own LoaderData (the +/// kernel image and these buffers) lands there and stays reserved. fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind { if (!d.attribute.wb) return .mmio; return switch (d.@"type") { - .conventional_memory => .usable, - .boot_services_code, .boot_services_data => .reclaimable, + .conventional_memory, .boot_services_code, .boot_services_data => .usable, .acpi_reclaim_memory => .acpi_tables, .acpi_memory_nvs => .acpi_nvs, .memory_mapped_io, .memory_mapped_io_port_space => .mmio, diff --git a/src/main.zig b/src/main.zig index ac8fd50..935d4fe 100644 --- a/src/main.zig +++ b/src/main.zig @@ -52,25 +52,22 @@ fn kmain(boot_info: *const BootInfo) noreturn { // own MemoryRegion, so this is a plain slice — no firmware layout in sight. const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(boot_info.memory_map.regions))[0..boot_info.memory_map.len]; var usable_pages: u64 = 0; - var reclaim_pages: u64 = 0; var reserved_pages: u64 = 0; // reserved RAM only — MMIO is device space, not RAM for (regions) |r| { switch (r.kind) { .usable => usable_pages += r.pages, - .reclaimable => reclaim_pages += r.pages, .reserved, .acpi_tables, .acpi_nvs => reserved_pages += r.pages, .mmio => {}, } } - const total_pages = usable_pages + reclaim_pages + reserved_pages; + const total_pages = usable_pages + reserved_pages; const total_bytes = total_pages * danos.page_size; const gib = 1 << 30; con.write("\ndanos: physical memory\n"); con.print(" total RAM : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) }); - con.print(" usable : {d} MiB - free now; owned by the frame allocator\n", .{mib(usable_pages)}); - con.print(" reclaimable: {d} MiB - UEFI boot-services memory, free after exit\n", .{mib(reclaim_pages)}); - con.print(" reserved : {d} MiB - kernel image, ACPI, runtime services\n", .{mib(reserved_pages)}); + con.print(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)}); + con.print(" reserved : {d} MiB - kernel image, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)}); con.print(" regions : {d} - entries in the firmware memory map\n", .{regions.len}); // Bring up the physical frame allocator over that map, and prove it works: diff --git a/src/pmm.zig b/src/pmm.zig index 01635fb..6b34049 100644 --- a/src/pmm.zig +++ b/src/pmm.zig @@ -58,12 +58,13 @@ fn regions(map: danos.MemoryMap) []const danos.MemoryRegion { pub fn init(map: danos.MemoryMap) void { const regs = regions(map); - // 1. Size the bitmap to cover every frame up to the highest usable address. - // Reserved/MMIO spans above that are simply outside the map and never - // allocatable. + // 1. Size the bitmap to cover every frame up to the highest RAM address — + // including reserved RAM, so those frames are trackable (e.g. to free the + // boot buffers later). Only MMIO (device address space) is excluded. + // Everything starts unallocatable; usable regions are freed below. var highest: u64 = 0; for (regs) |r| { - if (r.kind != .usable) continue; + if (r.kind == .mmio) continue; const end = r.base + r.pages * page_size; if (end > highest) highest = end; } diff --git a/src/root.zig b/src/root.zig index 23203c0..5886922 100644 --- a/src/root.zig +++ b/src/root.zig @@ -41,13 +41,14 @@ pub const page_size = 4096; /// native memory description into these kinds, so the kernel never learns what /// booted it. [[arch]] keeps the same discipline for CPU code. pub const MemoryKind = enum(u32) { - /// Free RAM the kernel may allocate. + /// Free RAM the kernel may allocate. Each boot path folds its own transient + /// memory into this once it's genuinely free (e.g. the UEFI loader classifies + /// boot-services memory as usable after ExitBootServices), so the kernel never + /// has to know about boot-protocol-specific "reclaimable" states. usable, - /// Firmware, MMIO, the kernel image, our own boot buffers — never hand out. + /// Firmware, MMIO, the kernel image, our own boot buffers, the boot stack — + /// never hand out. reserved, - /// Usable once the kernel is done with boot-time structures (e.g. UEFI boot - /// services memory, which is free after ExitBootServices). - reclaimable, /// ACPI tables: parse, then reclaim. acpi_tables, /// ACPI non-volatile storage: preserve across sleep, do not allocate.