reclaiming uefi memory

This commit is contained in:
2026-07-03 19:31:32 +01:00
parent 50f3610768
commit 21b9691486
6 changed files with 79 additions and 48 deletions
+18 -5
View File
@@ -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 distinct consecutive addresses, and the count returning to its start after freeing
are the three signals that init, alloc and free are all correct. 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) ## What's next (not done here)
- **Contiguous allocation** — scan for N consecutive free bits — for callers that - **Contiguous allocation** — scan for N consecutive free bits — for callers that
need physically adjacent frames. need physically adjacent frames.
- **Consumers**: the virtual memory manager / page tables and then the kernel heap - **A kernel stack for task 0**, so the boot stack's region can be freed too (and
will be the first real users, each asking `alloc()` for frames. for the clean stack user mode wants).
- **Reclaiming `reclaimable`** (UEFI boot-services) memory, and eventually the - **Freeing the `reserved` `loader_data`** (the boot-time map buffers) once the
`reserved` `loader_data` (kernel image, boot buffers) once nothing needs it — kernel is done reading the memory map.
see the deferred list in [memory-map.md](memory-map.md).
+30 -22
View File
@@ -32,8 +32,7 @@ Defined in `src/root.zig`, the shared loader↔kernel contract:
```zig ```zig
pub const MemoryKind = enum(u32) { pub const MemoryKind = enum(u32) {
usable, // free RAM the kernel may allocate usable, // free RAM the kernel may allocate
reserved, // firmware / kernel image — real RAM, but never hand out reserved, // firmware / kernel image / boot stack — real RAM, never hand out
reclaimable, // usable once boot-time structures are done with
acpi_tables, // parse, then reclaim acpi_tables, // parse, then reclaim
acpi_nvs, // preserve across sleep acpi_nvs, // preserve across sleep
mmio, // device registers / reserved address space — not RAM at all 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`: Two functions in `src/efi.zig`, called from `exitBootServices`:
- **`classify`** maps each UEFI descriptor to a `MemoryKind`: - **`classify`** maps each UEFI descriptor to a `MemoryKind`:
`conventional_memory → usable`; `boot_services_code`/`boot_services_data → `conventional_memory` **and** `boot_services_code`/`boot_services_data → usable`;
reclaimable` (free once we've exited); `acpi_reclaim_memory → acpi_tables`; `acpi_reclaim_memory → acpi_tables`; `acpi_memory_nvs → acpi_nvs`; **everything
`acpi_memory_nvs → acpi_nvs`; **everything else → reserved** (the safe default). else → reserved** (the safe default). Our own `loader_data` — the kernel image and
Our own `loader_data` — the kernel image and these buffers — falls into these buffers — falls into `reserved`.
`reserved`, so it won't be handed out until the kernel deliberately reclaims it.
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` One subtlety: **a region that isn't writeback-cacheable (the descriptor's `wb`
attribute) is classified `mmio` regardless of type.** UEFI overloads attribute) is classified `mmio` regardless of type.** UEFI overloads
@@ -126,18 +133,17 @@ for (regions) |r| {
``` ```
danos: physical memory danos: physical memory
total RAM : 0.12 GiB (127 MiB) - RAM the firmware reported total RAM : 0.12 GiB (127 MiB) - RAM the firmware reported
usable : 77 MiB - free now; owned by the frame allocator usable : 121 MiB - free RAM (incl. reclaimed boot-services memory)
reclaimable: 44 MiB - UEFI boot-services memory, free after exit reserved : 6 MiB - kernel image, boot stack, ACPI, runtime services
reserved : 6 MiB - kernel image, ACPI, runtime services regions : 28 - entries in the firmware memory map
regions : 35 - entries in the firmware memory map
``` ```
The `usable` figure is only ~77 of ~127 MiB because most of the rest is `usable` is ~121 of ~127 MiB because the loader already folded the boot-services
`reclaimable` boot-services memory — real RAM we'll take back once we implement memory into it — so the frame allocator gets it all with no special step. The ~6 MiB
reclaiming, not memory that's gone. `total` counts only writeback-cacheable RAM, `reserved` is the kernel image, the boot stack's region, ACPI, and runtime services.
so the ~12 GiB PCIe address hole is excluded (it's `mmio`), and the three RAM `total` counts only writeback-cacheable RAM, so the ~12 GiB PCIe address hole is
categories summing back to the firmware's total is the sanity check that nothing excluded (it's `mmio`), and the RAM categories summing back to the firmware's total
was dropped. is the sanity check that nothing was dropped.
## How Raspberry Pi will fit ## How Raspberry Pi will fit
@@ -150,11 +156,13 @@ never knows the difference.
## What's next ## What's next
This page is plumbing plus classification only. The map's first consumer, the This page is plumbing plus classification only. The map's first consumer, the
**physical frame allocator**, is built directly on the `usable` regions here — **physical frame allocator**, is built directly on the `usable` regions here — which
see [frame-allocator.md](frame-allocator.md). Still to come after that: 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` - Freeing the `reserved` `loader_data` (these boot-time buffers) once the kernel is
(kernel image, these buffers) once the kernel is done reading them. done reading the map.
- Paging / the kernel's own page tables, then a heap. - 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. See the roadmap in [efi.md](efi.md) for where this sits in the boot flow.
+17 -6
View File
@@ -269,6 +269,12 @@ fn exitBootServices(bs: *uefi.tables.BootServices) !danos.MemoryMap {
/// never sees UEFI's vocabulary — the same seam the framebuffer already uses. /// never sees UEFI's vocabulary — the same seam the framebuffer already uses.
fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap { fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
const regions: [*]danos.MemoryRegion = @ptrCast(@alignCast(out.ptr)); 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 count: usize = 0;
var i: usize = 0; var i: usize = 0;
while (i < map.info.len) : (i += 1) { 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 = const d: *const uefi.tables.MemoryDescriptor =
@ptrCast(@alignCast(map.ptr + i * map.info.descriptor_size)); @ptrCast(@alignCast(map.ptr + i * map.info.descriptor_size));
if (d.number_of_pages == 0) continue; 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. // Coalesce with the previous region if it's the same kind and contiguous.
if (count > 0) { 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` /// 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 /// 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. /// 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 /// Boot-services memory is folded straight into `usable`: we've already called
/// stays reserved until the kernel reclaims it. /// 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 { fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind {
if (!d.attribute.wb) return .mmio; if (!d.attribute.wb) return .mmio;
return switch (d.@"type") { return switch (d.@"type") {
.conventional_memory => .usable, .conventional_memory, .boot_services_code, .boot_services_data => .usable,
.boot_services_code, .boot_services_data => .reclaimable,
.acpi_reclaim_memory => .acpi_tables, .acpi_reclaim_memory => .acpi_tables,
.acpi_memory_nvs => .acpi_nvs, .acpi_memory_nvs => .acpi_nvs,
.memory_mapped_io, .memory_mapped_io_port_space => .mmio, .memory_mapped_io, .memory_mapped_io_port_space => .mmio,
+3 -6
View File
@@ -52,25 +52,22 @@ fn kmain(boot_info: *const BootInfo) noreturn {
// own MemoryRegion, so this is a plain slice — no firmware layout in sight. // 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]; const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(boot_info.memory_map.regions))[0..boot_info.memory_map.len];
var usable_pages: u64 = 0; var usable_pages: u64 = 0;
var reclaim_pages: u64 = 0;
var reserved_pages: u64 = 0; // reserved RAM only — MMIO is device space, not RAM var reserved_pages: u64 = 0; // reserved RAM only — MMIO is device space, not RAM
for (regions) |r| { for (regions) |r| {
switch (r.kind) { switch (r.kind) {
.usable => usable_pages += r.pages, .usable => usable_pages += r.pages,
.reclaimable => reclaim_pages += r.pages,
.reserved, .acpi_tables, .acpi_nvs => reserved_pages += r.pages, .reserved, .acpi_tables, .acpi_nvs => reserved_pages += r.pages,
.mmio => {}, .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 total_bytes = total_pages * danos.page_size;
const gib = 1 << 30; const gib = 1 << 30;
con.write("\ndanos: physical memory\n"); 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(" 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(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\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, boot stack, ACPI, runtime services\n", .{mib(reserved_pages)});
con.print(" reserved : {d} MiB - kernel image, ACPI, runtime services\n", .{mib(reserved_pages)});
con.print(" regions : {d} - entries in the firmware memory map\n", .{regions.len}); 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: // Bring up the physical frame allocator over that map, and prove it works:
+5 -4
View File
@@ -58,12 +58,13 @@ fn regions(map: danos.MemoryMap) []const danos.MemoryRegion {
pub fn init(map: danos.MemoryMap) void { pub fn init(map: danos.MemoryMap) void {
const regs = regions(map); const regs = regions(map);
// 1. Size the bitmap to cover every frame up to the highest usable address. // 1. Size the bitmap to cover every frame up to the highest RAM address —
// Reserved/MMIO spans above that are simply outside the map and never // including reserved RAM, so those frames are trackable (e.g. to free the
// allocatable. // boot buffers later). Only MMIO (device address space) is excluded.
// Everything starts unallocatable; usable regions are freed below.
var highest: u64 = 0; var highest: u64 = 0;
for (regs) |r| { for (regs) |r| {
if (r.kind != .usable) continue; if (r.kind == .mmio) continue;
const end = r.base + r.pages * page_size; const end = r.base + r.pages * page_size;
if (end > highest) highest = end; if (end > highest) highest = end;
} }
+6 -5
View File
@@ -41,13 +41,14 @@ pub const page_size = 4096;
/// native memory description into these kinds, so the kernel never learns what /// native memory description into these kinds, so the kernel never learns what
/// booted it. [[arch]] keeps the same discipline for CPU code. /// booted it. [[arch]] keeps the same discipline for CPU code.
pub const MemoryKind = enum(u32) { 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, 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, 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: parse, then reclaim.
acpi_tables, acpi_tables,
/// ACPI non-volatile storage: preserve across sleep, do not allocate. /// ACPI non-volatile storage: preserve across sleep, do not allocate.