Memory Layout Cheat-Sheet
When you debug a kernel that won't load, place a virtio device on the MMIO bus, or trace the boot
sequence, the question is always the same: what lives at this guest physical address? This page is
the answer key — the x86_64 and aarch64 guest-memory maps, every layout constant with the
file it comes from, and an ASCII memory map you can hold a KVM_EXIT_MMIO address up against to
classify it in five seconds.
Reach for this while tracing the boot sequence, working through Level 6, or reasoning about guest memory.
Warning — every constant here is version-sensitive. Layout constants live in per-arch source files that change between branches and across the recent crate-merge refactor. Treat every value below as "the role, and roughly where it sits," not gospel, and verify the literal in your checkout:
# x86_64 constants (the source of truth — read it, don't trust this page): rg -n "START|GAP|ADDR|_BASE|HIMEM|ZERO_PAGE|CMDLINE" src/vmm/src/arch/x86_64/layout.rs # aarch64 constants: rg -n "DRAM_MEM_START|FDT|GIC|MEM_START|MMIO" src/vmm/src/arch/aarch64/layout.rs # Where memory regions are actually created and registered with KVM: rg -n "create_guest_memory|arch_memory_regions|MMIO_MEM_START" src/vmm/src/Each row below is tagged (verify in
arch/<arch>/layout.rson your branch) as a reminder.
x86_64 guest-memory layout
The x86_64 map is two RAM regions split by an MMIO gap just below 4 GiB. Low addresses hold the boot scaffolding the kernel expects (the zero page, the command line, the GDT/page tables, the boot stack); the kernel itself loads at 1 MiB; device MMIO windows and the APIC live in the gap; any RAM beyond what fits below the gap is "high RAM" above 4 GiB.
| Constant (role) | Typical value | What lives there | Where from |
|---|---|---|---|
ZERO_PAGE_START | 0x7000 | The boot_params / "zero page" (rsi points here at boot) | arch/x86_64/layout.rs (verify on your branch) |
| Boot stack | ~0x8ff0 | Initial stack the early kernel uses | arch/x86_64/layout.rs (verify) |
| GDT / IDT / page tables | low, sub-0x20000 | The Global Descriptor Table and identity page tables that put the vCPU in long mode | arch/x86_64/ regs/gdt setup (verify) |
CMDLINE_START | 0x20000 | The kernel command line; its address goes in the zero page | arch/x86_64/layout.rs (verify) |
HIMEM_START | 0x100000 (1 MiB) | Where the kernel image is loaded; start of usable "high" low-RAM | arch/x86_64/layout.rs (verify) |
| (low RAM) | HIMEM_START → MMIO_GAP | Guest RAM below the gap (kernel, initrd, early allocations) | arch_memory_regions() (verify) |
| MMIO gap base | ~0xC000_0000 (below 4 GiB) | Start of the reserved window for virtio-MMIO device registers | arch/x86_64/layout.rs (verify) |
| virtio-MMIO windows | within the gap | One fixed register block + IRQ per virtio device | MMIO bus & device manager (verify) |
| APIC / IOAPIC | 0xFEC0_0000 (IOAPIC), 0xFEE0_0000 (LAPIC) | Interrupt-controller MMIO (architectural x86 addresses) | interrupts & irqchip (verify) |
| 4 GiB boundary | 0x1_0000_0000 | Top of the 32-bit space; RAM resumes above here | — |
| High RAM | ≥ 0x1_0000_0000 | Guest RAM that didn't fit below the gap (only when configured mem > the sub-gap capacity) | arch_memory_regions() (verify) |
Note — the e820 map is how the guest learns this. The VMM doesn't just place RAM, it describes it: it writes an e820 memory map into the zero page so the kernel knows which physical ranges are usable RAM versus the reserved MMIO gap. A guest that thinks the gap is RAM (or can't find its high RAM) is an e820 bug, not a placement bug. See
../masterclass/the-boot-process/lab-02-zero-page-and-e820.md.
x86_64 ASCII memory map
guest physical address
0xFFFF_FFFF ─────────────────────────────────────────── 4 GiB
↑ (high RAM continues above 4 GiB if configured mem is large)
0x1_0000_0000 ═══════════════════════════════════════════ HIGH RAM ▲ above 4 GiB
guest RAM region 2 (only if mem > sub-gap capacity) │
0x1_0000_0000 ─────────────────────────────────────────── 4 GiB ──────┘
0xFEE0_0000 ┌─────────────────────────────┐ Local APIC (LAPIC) MMIO
0xFEC0_0000 ├─────────────────────────────┤ IOAPIC MMIO
│ . . . MMIO GAP . . . │ reserved, below 4 GiB
~0xC000_0000 ├─────────────────────────────┤ ← MMIO gap base
│ virtio-MMIO device windows │ net / block / vsock / … (fixed block + IRQ each)
└─────────────────────────────┘
╔═════════════════════════════╗ LOW RAM (guest RAM region 1)
║ initrd / allocations ║
0x0010_0000 ║─ HIMEM_START (1 MiB) ────────║ ← kernel image loaded here
║ sub-1MiB scaffolding: ║
0x0002_0000 ║─ CMDLINE_START ──────────────║ kernel command line
0x0000_8ff0 ║ boot stack ║
║ GDT / IDT / page tables ║ (low, set up for long mode)
0x0000_7000 ║─ ZERO_PAGE_START ────────────║ boot_params (rsi points here)
0x0000_0000 ╚═════════════════════════════╝
Warning: The MMIO-gap base and the exact low-RAM ceiling are the values most likely to differ on your branch. When you place a device or decode an MMIO exit,
rgthe gap constant —rg -n "MMIO|GAP|MEM_START" src/vmm/src/arch/x86_64/layout.rs— rather than trusting the~0xC000_0000above, which is illustrative.
aarch64 guest-memory layout
aarch64 is a different model. There is no zero page and no e820 — instead the VMM builds a
Flattened Device Tree (FDT/DTB) describing memory, CPUs, and devices, and passes its address to
the kernel in register x0. The kernel is an arm64 PE Image (not a vmlinux ELF). Guest DRAM
starts high (DRAM_MEM_START), and the interrupt controller is the GIC, not an x86 APIC.
| Constant (role) | Typical value | What lives there | Where from |
|---|---|---|---|
| MMIO / device region | low (below DRAM) | virtio-MMIO device windows + the GIC distributor/redistributor | arch/aarch64/layout.rs (verify on your branch) |
GIC (GICv2/GICv3) | low, in the device region | The Generic Interrupt Controller MMIO (created via KVM) | interrupts & irqchip (verify) |
DRAM_MEM_START | 0x8000_0000 (2 GiB) | Base of guest DRAM | arch/aarch64/layout.rs (verify) |
Kernel Image load | at/near DRAM_MEM_START | The arm64 PE kernel image | arch/aarch64/ loader (verify) |
| FDT / DTB | placed in DRAM (address → x0) | The device tree the kernel reads to discover memory/CPUs/devices | arch/aarch64/fdt.rs via vm-fdt FdtWriter (verify) |
| (guest RAM) | DRAM_MEM_START → +mem | Contiguous guest DRAM | arch_memory_regions() (verify) |
aarch64 ASCII memory map
guest physical address
↑ (guest DRAM continues upward for the configured memory size)
╔═════════════════════════════╗ GUEST DRAM
║ FDT / DTB (addr → x0) ║ device tree blob, placed in DRAM
║ kernel Image (arm64 PE) ║
0x8000_0000 ║─ DRAM_MEM_START (2 GiB) ─────║ ← base of guest RAM
╚═════════════════════════════╝
┌─────────────────────────────┐ DEVICE / MMIO region (below DRAM)
│ virtio-MMIO device windows │ net / block / vsock / … (FDT node each)
│ GIC distributor / redist. │ the interrupt controller
0x0000_0000 └─────────────────────────────┘
Note — FDT is the aarch64 analogue of the x86 zero page. Where x86 writes
boot_params+ e820 and pointsrsiat it, aarch64 builds an FDT (withvm-fdt'sFdtWriter) describing the same facts — RAM ranges, CPU count, each virtio-MMIO device's address/IRQ, the GIC — and pointsx0at it. A device the guest can't see on aarch64 is usually a missing or wrong FDT node, the way a missing x86 device is usually a wrong cmdlinevirtio_mmio.device=entry. See../masterclass/the-boot-process/lab-03-aarch64-fdt.md.
How a virtio device's address reaches the guest
The layout only matters because the guest has to find each device. The two arches tell it differently — and this is the single most common "my device isn't detected" debugging axis.
| x86_64 | aarch64 | |
|---|---|---|
| Where device windows sit | In the MMIO gap below 4 GiB | In the device region below DRAM |
| How the guest is told | Kernel cmdline: virtio_mmio.device=<SIZE>@<ADDR>:<IRQ> (one per device) | An FDT node per device (address + IRQ) |
| Who writes it | The VMM appends to boot_args; placement by MMIODeviceManager | The VMM emits FDT nodes via vm-fdt; placement by MMIODeviceManager |
| Deep dive | virtio transport (MMIO) | virtio transport (MMIO) |
# See the per-device MMIO placement and the cmdline/FDT wiring on your branch:
rg -n "virtio_mmio.device|MMIO_MEM_START|register_mmio|allocate" src/vmm/src/device_manager/
rg -n "FdtWriter|/virtio_mmio|interrupts" src/vmm/src/arch/aarch64/
Where each region comes from (the boot pipeline)
flowchart TD
A["machine-config: mem_size_mib"] --> B["arch_memory_regions() splits RAM<br/>around the MMIO gap (x86) / from DRAM_MEM_START (arm64)"]
B --> C["create_guest_memory: mmap + KVM_SET_USER_MEMORY_REGION"]
C --> D["linux-loader: copy kernel image to HIMEM_START (x86) / DRAM_MEM_START (arm64)"]
D --> E["x86: write boot_params @ ZERO_PAGE_START + e820 + cmdline @ CMDLINE_START<br/>arm64: build FDT, place in DRAM"]
E --> F["set initial regs: x86 rsi=ZERO_PAGE_START, rip=e_entry<br/>arm64 x0=FDT addr"]
F --> G["MMIODeviceManager places virtio devices in the gap / device region"]
Every box is rg-able: rg -n "arch_memory_regions|create_guest_memory|ZERO_PAGE_START|FdtWriter" src/vmm/src/.
The KVM_SET_USER_MEMORY_REGION step is where these host mmaps become guest RAM — see the
KVM ioctl cheat-sheet.
Reading exercise
From a Firecracker checkout:
# 1. Read the literal x86 constants and compare to the table above:
rg -n "ZERO_PAGE_START|CMDLINE_START|HIMEM_START|MMIO|GAP" src/vmm/src/arch/x86_64/layout.rs
# 2. Read the aarch64 base + GIC + FDT placement:
rg -n "DRAM_MEM_START|GIC|FDT|MEM_START" src/vmm/src/arch/aarch64/layout.rs
# 3. Find where RAM is split around the MMIO gap (the two-region logic):
rg -n "arch_memory_regions|MMIO_MEM_START|first_addr_past_32bits|GuestAddress" src/vmm/src/arch/
# 4. Find where the kernel is copied in and the entry point captured:
rg -n "load_kernel|e_entry|Elf|Image|HIMEM_START|DRAM_MEM_START" src/vmm/src/
Questions to answer from what you find:
- On x86_64, which constant is the kernel load address, and which is the zero-page address the boot
register
rsipoints at? - Why does x86_64 split guest RAM into two regions, while aarch64 has one contiguous DRAM region? What sits in the gap on x86?
- aarch64 has no zero page. What replaces it, in which register is its address passed, and which crate builds it?
- A guest doesn't detect a virtio-block device. Name the x86 mechanism and the aarch64 mechanism that tell the guest where the device's MMIO window is — and which one you'd check first on each arch.
- Which value in the x86 table is most likely to differ on your branch, and what
rgconfirms it?
Related references
- KVM ioctl Cheat-Sheet —
KVM_SET_USER_MEMORY_REGIONregisters these regions; the boot registers (rsi/x0,rip) are set withKVM_SET_REGS. - API Endpoint Map —
/machine-config(mem size),/boot-source(kernel, cmdline) feed this layout. - Glossary —
ZERO_PAGE_START,HIMEM_START,DRAM_MEM_START, MMIO gap, FDT, e820. - The boot sequence and guest memory management — the prose behind these maps.
- Level 6 and
../masterclass/the-boot-process/index.md— the labs that make you instrument this.
Next: Firecracker vs. Other VMMs — why a VMM this minimal exists at all.