8.4 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 architecture.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 system/boot-handoff.zig, the shared loader↔kernel contract:
pub const MemoryKind = enum(u32) {
usable, // free RAM the kernel may allocate
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
};
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).
BootInformation carries it alongside the framebuffer (trimmed here to the
fields this page is about — the full struct has since grown the kernel's
PT_LOAD segments, the ACPI RSDP, and the initial-ramdisk span):
pub const BootInformation = extern struct {
framebuffer: Framebuffer,
memory_map: MemoryMap,
// ...kernel_segments, acpi_rsdp, initial_ramdisk_base/len
};
The loader side (UEFI)
Two functions in boot/efi.zig: exitBootServices calls convertMemoryMap,
which runs classify on each descriptor:
-
classifymaps each UEFI descriptor to aMemoryKind:conventional_memoryandboot_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; everything else → reserved (the safe default). Our ownloader_data— the kernel image and these buffers — falls intoreserved.Folding boot-services memory into
usableis 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, soconvertMemoryMapkeeps the single region containing the current stack pointerreserved. 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
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_information.memory_map;
const regions = @as(
[*]const boot_handoff.MemoryRegion,
@ptrFromInt(boot_handoff.physicalToVirtual(mm.regions)),
)[0..mm.len];
for (regions) |r| {
if (r.kind == .usable) usable_pages += r.pages;
}
(mm.regions is a physical address, so it's dereferenced through the physmap —
physicalToVirtual — since the kernel no longer runs under the loader's
identity map.)
kmain summarises the map to prove the handoff works. Booted in QEMU with
128 MiB, it reports:
/system/kernel: physical memory
total RAM : 0.12 GiB (127 MiB) - RAM the firmware reported
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
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
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 — which
already include the reclaimed boot-services memory the loader folded in (see
frame-allocator.md). Of the two items once listed here, one is done:
- Freeing the
reservedloader_data(these boot-time buffers) once the kernel is done reading the map — still open: the frame allocator's bitmap tracks those frames so they can be freed, but nothing frees them yet. - Capturing the ACPI RSDP from the UEFI configuration table before exit (the same "grab it before ExitBootServices" pattern) — done: the loader stows it in the boot handoff, and ACPI parsing consumes it from there (acpi.md).
See the roadmap in efi.md for where this sits in the boot flow.