# 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:

1. **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](arch.md) for the same "keep the kernel
   platform-agnostic" principle applied to CPU code.
2. **We already established the better pattern.** The loader doesn't hand the
   kernel a raw UEFI GOP either — [`queryFramebuffer`](gop.md) converts it to
   danos's own `Framebuffer`. 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:

```zig
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).

`BootInfo` carries it alongside the framebuffer:

```zig
pub const BootInfo = extern struct {
    framebuffer: Framebuffer,
    memory_map: MemoryMap,
};
```

## The loader side (UEFI)

Two functions in `boot/efi.zig`, called from `exitBootServices`:

- **`classify`** maps each UEFI descriptor to a `MemoryKind`:
  `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
  `reserved_memory_type` for 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.
- **`convertMemoryMap`** walks the UEFI descriptors (striding by
  `descriptor_size`, *not* `@sizeOf`), classifies each, and writes danos
  `MemoryRegion`s 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:

1. `getMemoryMapInfo` to size things, then `allocatePool` **two** LoaderData
   buffers — one for the raw UEFI map, one for the converted regions. Allocating
   now, before exit, is mandatory.
2. `getMemoryMap` then `exitBootServices(key)`. If either fails (allocating can
   perturb the map and invalidate the key), free both buffers and retry.
3. **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:

```zig
const mm = boot_info.memory_map;
const regions = @as([*]const system.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     : 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](frame-allocator.md)). Still to come:

- 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.
