6.9 KiB
The memory map
Before a kernel can manage memory, it has to know what memory exists: which physical address ranges are real RAM it may use, and which are firmware, hardware registers, or already occupied. That inventory is the memory map, and the firmware is the only thing that knows it. This page covers how danos gets that map from the firmware and hands it to the kernel — deliberately without dragging UEFI into the kernel.
Why not just pass UEFI's map through?
UEFI hands the loader a perfectly good memory map. The tempting shortcut is to forward it to the kernel as-is. We don't, for two reasons:
- It would tie the kernel to UEFI. The kernel would compare against UEFI's memory-type numbers and walk the array using UEFI's variable descriptor stride. That's UEFI vocabulary bleeding across the handoff — and danos wants to boot on systems that have no UEFI at all (a Raspberry Pi describes its memory with a device tree instead). See arch.md for the same "keep the kernel platform-agnostic" principle applied to CPU code.
- We already established the better pattern. The loader doesn't hand the
kernel a raw UEFI GOP either —
queryFramebufferconverts it to danos's ownFramebuffer. The memory map follows the same discipline.
So the boundary is: each boot path translates its native memory description into danos's own neutral format, and the kernel only ever sees that.
The neutral format
Defined in src/root.zig, the shared loader↔kernel contract:
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
acpi_tables, // parse, then reclaim
acpi_nvs, // preserve across sleep
mmio, // device registers / reserved address space — not RAM at all
};
pub const MemoryRegion = extern struct {
base: u64, // physical start
pages: u64, // length in page_size (4 KiB) units
kind: MemoryKind,
_pad: u32 = 0,
};
pub const MemoryMap = extern struct {
regions: usize, // pointer to a [len]MemoryRegion
len: usize,
};
MemoryKind is danos's own vocabulary — not UEFI's ~15 types, just the
distinctions the kernel actually acts on. And because danos defines MemoryRegion
itself, @sizeOf is authoritative: the kernel walks a plain []MemoryRegion with
no variable-stride subtlety (that stride problem is a UEFI-ism, and it stays in the
loader).
BootInfo carries it alongside the framebuffer:
pub const BootInfo = extern struct {
framebuffer: Framebuffer,
memory_map: MemoryMap,
};
The loader side (UEFI)
Two functions in src/efi.zig, called from exitBootServices:
-
classifymaps each UEFI descriptor to aMemoryKind: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 ownloader_data— the kernel image and these buffers — falls intoreserved, so it won't be handed out until the kernel deliberately reclaims it.One subtlety: a region that isn't writeback-cacheable (the descriptor's
wbattribute) is classifiedmmioregardless of type. UEFI overloadsreserved_memory_typefor both reserved RAM and reserved address-space windows (PCIe config space, device BARs); the cache attribute is what actually tells them apart, since only real RAM is writeback-cacheable. Without this, a QEMU q35 guest reports ~12 GiB of "reserved" that is really a PCIe address hole near the 1 TB mark — not memory at all. -
convertMemoryMapwalks the UEFI descriptors (striding bydescriptor_size, not@sizeOf), classifies each, and writes danosMemoryRegions into an output buffer, coalescing adjacent same-kind regions.
The ordering that makes it correct
This is the fiddly part, dictated by two UEFI rules: you can only allocate memory
before ExitBootServices, and the memory map is only final at the moment you
exit (its "key" proves you've seen the latest state). So exitBootServices does,
per attempt:
getMemoryMapInfoto size things, thenallocatePooltwo LoaderData buffers — one for the raw UEFI map, one for the converted regions. Allocating now, before exit, is mandatory.getMemoryMapthenexitBootServices(key). If either fails (allocating can perturb the map and invalidate the key), free both buffers and retry.- After the exit succeeds, convert. Conversion is pure computation on memory we already hold — no boot-services calls — so it's safe once services are gone.
Both buffers are LoaderData, which survives ExitBootServices, so the converted
array the kernel is pointed at stays valid. (The raw UEFI buffer is just scratch
for the conversion.)
The kernel side
The kernel receives a plain array and reads it with zero UEFI knowledge:
const mm = boot_info.memory_map;
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
for (regions) |r| {
if (r.kind == .usable) usable_pages += r.pages;
}
kmain summarises the map to prove the handoff works. Booted in QEMU with
128 MiB, it reports:
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
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.
How Raspberry Pi will fit
No UEFI there, but the boundary is unchanged. The Pi's firmware jumps into the
kernel with a device-tree blob; the AArch64 entry code will parse its
/memory and /reserved-memory nodes and produce the same MemoryRegion
array. The kernel's memory code — the frame allocator and everything above it —
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. Still to come after that:
- Reclaiming
reclaimableregions, and carefully freeingreservedloader_data(kernel image, these buffers) once the kernel is done reading them. - Paging / the kernel's own page tables, then a heap.
See the roadmap in efi.md for where this sits in the boot flow.