5.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 / MMIO / kernel image — never hand out
reclaimable, // usable once boot-time structures are done with
acpi_tables, // parse, then reclaim
acpi_nvs, // preserve across sleep
};
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 memory type 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.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;
}
Today kmain just prints the region count and total usable RAM — enough to prove
the handoff works. Booted in QEMU with 128 MiB, it reports something like:
mem regions: 35
usable RAM : 77 MiB
with the balance being reclaimable boot-services memory (~44 MiB) and a large
reserved span that is mostly MMIO address space, not RAM. Those figures summing
back to ~128 MiB 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.