Level 6: The Boot Process and Guest Memory
You have read the vCPU run loop and watched VM exits resolve (Level 4), and you
have learned how the project defends itself with tests (Level 5). Now you learn
the moment before the run loop spins: how a microVM goes from "a KVM_CREATE_VM fd and some mmap'd
host pages" to "a Linux kernel executing its first instruction at e_entry in 64-bit long mode." This
is the boot path, and it is where Firecracker's "do almost nothing" philosophy is most visible —
there is no BIOS, no bootloader, no firmware, no PCI enumeration. Firecracker hands the kernel a region
of memory, a structured set of boot parameters, a command line, and a register file, and lets go.
A kernel does not boot itself. Something has to parse the ELF, copy its segments into guest physical memory, decide where in that memory the RAM is and where the holes are, write a machine-readable description of all of that into a structure the kernel knows how to read, and set the CPU into exactly the state the kernel's entry code assumes. On x86_64 that description is the boot_params / "zero page" with its e820 memory map; on aarch64 it is a flattened device tree (FDT/DTB). Getting any byte of it wrong does not produce a Rust panic — it produces a guest that hangs, triple-faults, or prints garbage to a serial port that may not even be wired up yet. This is unforgiving code, and it is some of the most rewarding in the whole codebase to understand.
By the end of this level you will be able to stand at InstanceStart and narrate, hop by hop, how the
builder allocates guest memory, loads the kernel with linux-loader, lays out the boot structures,
programs the initial vCPU register and segment state for long mode, and only then starts the vCPU
threads — and you will be able to do it for both x86_64 and aarch64.
Learning Objectives
By the end of Level 6 you must be able to:
- Trace the boot path from
build_microvm_for_bootthrough guest-memory creation, kernel loading, boot-parameter configuration, and initial vCPU register/segment programming, naming the function and file that owns each step (Lab 6.1). - Explain how Firecracker loads an uncompressed vmlinux ELF with
linux-loader'sElfloader: howPT_LOADsegments are copied into guest memory and howe_entrybecomes the guest's firstrip. - Describe the x86_64 guest memory layout — the sub-1 MiB region, the zero page, the kernel command line, the GDT and page tables, the boot stack, the MMIO gap below 4 GiB, and high RAM above 4 GiB — and state the constant that pins each (Lab 6.2).
- Explain the boot_params / zero page: the e820 memory map, the command-line pointer, and the
initrd pointer, and how
linux-loader'sLinuxBootConfiguratorwrites them. - State the initial vCPU state for 64-bit long mode:
rip = e_entry,rsi = ZERO_PAGE_START, the control registers (cr0PE|PG,cr4PAE,eferLME|LMA), the GDT/segment descriptors, and the identity-mapped first GiB of page tables. - Explain how guest RAM is allocated on the host (
mmap) and registered with KVM viaKVM_SET_USER_MEMORY_REGIONthroughvm-memory'sGuestMemoryMmap, and why the MMIO gap forces memory to be split into regions. - Describe the aarch64 boot path and how it differs: the arm64 PE
Image, the FDT passed inx0,DRAM_MEM_START, and the absence of a zero page. - Experiment with boot configuration —
boot_args,console=ttyS0,reboot=k,panic=1, aninitrd, and thevirtio_mmio.device=...advertisements — and predict their effect on the guest (Lab 6.3).
The Boot Path, End to End
Booting a microVM is a strictly ordered sequence inside the VMM thread, driven by the builder when
the StartMicroVm action fires. Nothing here runs on a vCPU thread — the vCPU threads do not exist yet.
The builder constructs the entire machine in memory, programs each vCPU's initial register file, and
then unleashes the vCPU threads into their KVM_RUN loops.
flowchart TD
A["VmmAction::StartMicroVm<br/>(builder.rs)"] --> B["create_vmm_and_vcpus:<br/>open /dev/kvm, KVM_CREATE_VM"]
B --> C["allocate guest memory<br/>host mmap → GuestMemoryMmap<br/>→ KVM_SET_USER_MEMORY_REGION"]
C --> D["load_kernel:<br/>linux-loader Elf::load()<br/>copy PT_LOAD segments, get e_entry"]
D --> E["load_initrd (optional):<br/>copy initrd image high in RAM"]
E --> F["build the kernel cmdline<br/>(linux-loader Cmdline)"]
F --> G["configure_system / boot params:<br/>e820 map + zero page + cmdline ptr<br/>+ MPTable/ACPI (x86) or FDT (aarch64)"]
G --> H["per vCPU: configure regs/sregs<br/>rip=e_entry, rsi=zero page,<br/>long mode, GDT, page tables"]
H --> I["attach devices<br/>(MMIO bus, serial, virtio)"]
I --> J["start vCPU threads<br/>each enters KVM_RUN"]
J --> K["guest executes first instruction<br/>at e_entry"]
Read it as five phases:
- VM and memory (the substrate): open
/dev/kvm,KVM_CREATE_VM, allocate host RAM withmmap, wrap it in aGuestMemoryMmap, and register each region with KVM viaKVM_SET_USER_MEMORY_REGION. The guest's physical address space now exists. - Kernel load (the payload):
linux-loader'sElfloader parses the uncompressedvmlinux, copies eachPT_LOADsegment to its guest physical address, and returns the entry pointe_entry. - Boot structures (the contract): write the structures the kernel's entry code reads — on x86_64
the zero page (
boot_params) with the e820 map, the command line atCMDLINE_START, and the initrd pointer; the CPU topology table (MPTable and/or ACPI); on aarch64 the FDT. - vCPU programming (the handshake): for each vCPU, set the general-purpose registers (
rip,rsi/x0), the special registers (control registers, segment descriptors, GDT base), and install minimal identity-mapped page tables so the kernel starts in long mode with the first GiB mapped. - Devices, then go: attach the MMIO/PIO device managers, then spawn the vCPU threads — each enters
KVM_RUNand the guest runs.
Note: The order is load-bearing. Boot params must be written after the cmdline and initrd are placed (because they store pointers to them), and the vCPU registers must be programmed after the kernel is loaded (because
ripise_entry, which the ELF parse produces). When you trace this in Lab 6.1, watch the ordering as carefully as the data.
The x86_64 Guest Memory Layout
Guest physical memory is not a flat slab. The kernel, the boot structures, and the holes that devices live in all sit at fixed, agreed-upon addresses. Firecracker pins these as constants. The names below are the roles; confirm the exact identifiers on your branch — they have been renamed before.
# The layout constants live here. Run this; do not trust the names from memory.
rg -n "START|GAP|HIMEM|CMDLINE|ZERO_PAGE|MMIO|MEM_32BIT|FIRST_ADDR" \
src/vmm/src/arch/x86_64/layout.rs
GUEST PHYSICAL ADDRESS SPACE (x86_64) (constants — verify on your branch)
0x0000_0000 ┌────────────────────────────────────────────┐
│ real-mode IVT / BIOS data area (unused) │
0x0000_7000 │ ZERO_PAGE_START — boot_params (zero page) │ ← rsi points here
0x0000_8000 │ page tables (PML4/PDPT/PD), boot GDT │
0x0000_8ff0 │ BOOT_STACK_POINTER (boot stack grows down) │
0x0002_0000 │ CMDLINE_START — kernel command line │
... │ (free low memory) │
0x0010_0000 │ HIMEM_START (1 MiB) — vmlinux loaded here │ ← e_entry near/at here
│ kernel image (PT_LOAD segments) │
... │ guest RAM continues upward │
│ ... up to the 32-bit MMIO gap ... │
0xD000_0000 │ MMIO_MEM_START — virtio-MMIO device windows │ ← (gap, below 4 GiB)
... │ MMIO gap (devices, no RAM here) │
0x1_0000_0000│ 4 GiB boundary │
│ HIGH RAM (if mem_size pushes past the gap) │ ← remaining guest RAM
└────────────────────────────────────────────┘
The two structural facts to internalize:
- The sub-1 MiB region is crowded. The zero page, the boot GDT, the initial page tables, and the
boot stack all live below
HIMEM_START(1 MiB), in real-mode-addressable territory. The kernel image itself loads at 1 MiB. - There is a hole below 4 GiB. Guest RAM cannot extend continuously to 4 GiB, because the region
just under 4 GiB is reserved as the MMIO gap where memory-mapped devices (virtio-MMIO windows, the
APIC, etc.) live. If the configured
mem_size_mibis larger than the gap allows, the remainder of RAM is placed above 4 GiB as "high RAM." This split is why guest memory is multiple regions, not one.
# Where the RAM regions and the MMIO gap are computed (function names vary — verify).
rg -n "arch_memory_regions|MMIO_MEM_START|MEM_32BIT_GAP|first_usable|GuestMemoryMmap" \
src/vmm/src/arch/x86_64/ src/vmm/src/vstate/memory.rs
The full layout cheat-sheet, including the constants for both architectures side by side, lives in the memory-layout cheat-sheet and is explained in depth in the guest memory management deep dive.
How Guest Memory Is Allocated and Registered
Firecracker does not ask KVM for memory. It allocates anonymous host memory with mmap, hands the
host virtual address to KVM, and tells KVM "this host range is this guest physical range." That
mapping is one KVM_SET_USER_MEMORY_REGION ioctl per region.
HOST GUEST PHYSICAL
┌──────────────────────────┐ ┌──────────────────────────┐
│ mmap(...) → userspace VA │ KVM_SET_USER_ │ guest_phys_addr 0x0... │
│ (anonymous, MAP_PRIVATE) │ MEMORY_REGION │ memory_size = region len │
│ userspace_addr ───────────┼───────────────────►│ slot N │
└──────────────────────────┘ └──────────────────────────┘
The vm-memory crate's GuestMemoryMmap owns these regions and gives the rest of the codebase a safe
API to read/write guest physical addresses (GuestAddress) — bounds-checked, region-aware. Every later
subsystem (the kernel loader, the boot-params writer, virtio descriptor handling) goes through this
abstraction rather than touching raw pointers. The rust-vmm contract is covered in the
vm-memory chapter; the KVM ioctl shape is in the
KVM fundamentals deep dive.
# Memory creation + KVM registration. Find where regions are mmap'd and set on the VM.
rg -n "GuestMemoryMmap|from_ranges|mmap|set_user_memory_region|KVM_SET_USER_MEMORY|memory_init|create_guest_memory" \
src/vmm/src/vstate/memory.rs src/vmm/src/vstate/vm.rs src/vmm/src/builder.rs
Loading the Kernel: linux-loader's Elf Loader
Firecracker boots an uncompressed vmlinux ELF (not a compressed bzImage) on x86_64. The
linux-loader crate's Elf (a.k.a. loader::Elf) loader parses the ELF header, walks the program
headers, copies every PT_LOAD segment into guest memory at its physical address, and reports back the
entry point e_entry plus where the loadable image ended.
vmlinux ELF GUEST MEMORY
┌────────────────┐ ┌──────────────────────────┐
│ ELF header │ │ │
│ e_entry ──────┼──────────────►│ (becomes initial rip) │
│ Program headers│ │ │
│ PT_LOAD #0 ───┼──copy───────► │ segment @ p_paddr │
│ PT_LOAD #1 ───┼──copy───────► │ segment @ p_paddr │
│ ... │ │ │
└────────────────┘ └──────────────────────────┘
# Where Firecracker calls into linux-loader to load the kernel.
rg -n "Elf|linux_loader|load_kernel|KernelLoader|load\(|e_entry|kernel_load" \
src/vmm/src/arch/ src/vmm/src/builder.rs
The loader's job ends at "the bytes are in memory and here is e_entry." Everything that makes the
kernel able to run — the boot params, the page tables, the register state — is Firecracker's job, done
after the load. The linux-loader contract (the KernelLoader trait, Elf vs BzImage vs PE, and the
bootparam/Cmdline helpers) is the subject of the linux-loader chapter,
and the end-to-end sequence is in the boot sequence deep dive.
Required Reading
Read these before the labs. Each row tells you what to extract — read for that, not cover to cover.
| Source | What to extract |
|---|---|
src/vmm/src/arch/x86_64/layout.rs | The layout constants: zero page, cmdline, HIMEM, the MMIO gap, the boot stack. The single source of truth for where everything lives. |
src/vmm/src/arch/x86_64/regs.rs (verify name) | How GP registers, control registers, segment descriptors, the GDT, and identity-mapped page tables are programmed for long mode. |
src/vmm/src/arch/x86_64/ (e820 / boot params) | How the e820 map and the zero page (boot_params) are assembled and written. |
src/vmm/src/arch/aarch64/ | The FDT build, DRAM_MEM_START, the PE Image load, and x0. |
linux-loader docs (cargo doc -p linux-loader --open) | The KernelLoader trait, Elf::load, Cmdline, and LinuxBootConfigurator/bootparam. |
Kernel Documentation/arch/x86/boot.rst | The x86 boot protocol from the kernel's side: the zero page fields, the cmdline pointer, and what the kernel expects. |
# Confirm these exist on your branch before you start (paths drift after refactors).
ls src/vmm/src/arch/x86_64/ src/vmm/src/arch/aarch64/
rg -l "ZERO_PAGE_START|CMDLINE_START|HIMEM_START" src/vmm/src/arch/
cargo doc -p linux-loader --no-deps # then open the generated docs for the loaders
Source Code Areas to Inspect
| Area | Why |
|---|---|
src/vmm/src/builder.rs | The orchestration: build_microvm_for_boot calls memory creation, kernel load, boot config, vCPU setup, device attach, in order. |
src/vmm/src/arch/x86_64/ | Layout constants, register/segment setup, e820 + zero page, MPTable, the boot stack and GDT. |
src/vmm/src/arch/aarch64/ | FDT writer, the PE Image load path, GIC, DRAM_MEM_START, x0. |
src/vmm/src/vstate/memory.rs | GuestMemoryMmap creation, region computation, the MMIO gap split. |
src/vmm/src/vstate/vm.rs | KVM_SET_USER_MEMORY_REGION registration; the VM fd. |
src/vmm/src/vmm_config/boot_source.rs | The boot-source config (kernel path, boot_args, initrd) — the API surface of this level. |
acpi-tables/ + src/vmm/src/arch/ ACPI | RSDP/MADT generation; the ACPI path that is replacing MPTable. |
Key Types and Constants Quick Reference
Do not memorize these — run the rg and read the role. Names are stable enough to grep by role even
when paths move.
| Type / const | Role | Find it |
|---|---|---|
build_microvm_for_boot | The boot orchestration entry point in the builder | rg -n "fn build_microvm_for_boot" src/vmm/src/builder.rs |
GuestMemoryMmap | The vm-memory type owning all guest RAM regions | rg -n "GuestMemoryMmap" src/vmm/src/vstate/memory.rs |
GuestAddress | A typed guest physical address | rg -n "GuestAddress" src/vmm/src/ |
Elf / KernelLoader | linux-loader's vmlinux ELF loader and its trait | `rg -n "Elf |
Cmdline | linux-loader's kernel command-line builder | `rg -n "Cmdline |
ZERO_PAGE_START | Where boot_params (the zero page) lives; rsi target | rg -n "ZERO_PAGE_START" src/vmm/src/arch/x86_64/ |
CMDLINE_START | Guest address of the kernel command line | rg -n "CMDLINE_START" src/vmm/src/arch/x86_64/ |
HIMEM_START | 1 MiB; where the kernel image loads | rg -n "HIMEM_START" src/vmm/src/arch/x86_64/ |
MMIO_MEM_START / 32-bit gap | The MMIO gap below 4 GiB | `rg -n "MMIO_MEM_START |
DRAM_MEM_START | aarch64 RAM base (0x8000_0000) | rg -n "DRAM_MEM_START" src/vmm/src/arch/aarch64/ |
configure_system / boot params | Writes e820 + zero page (x86) / FDT (aarch64) | `rg -n "configure_system |
GitHub Issue Categories for Level 6
Boot and memory issues a Level 6 graduate can credibly engage with:
- Boot configuration / cmdline — incorrect or surprising default cmdline behavior, escaping, length
limits,
virtio_mmio.device=advertisement bugs. - Memory layout edge cases — large
mem_size_mibnear or past the MMIO gap, region splitting, alignment, huge-pages interactions. - Kernel/initrd loading — ELF parsing edge cases, initrd placement, error messages when a kernel is
the wrong format (e.g. a
bzImagewhere avmlinuxis expected). - aarch64 FDT — device-tree node correctness, FDT placement, missing properties.
- e820 / boot params — memory-map correctness, reserved-region reporting, ACPI-vs-MPTable migration.
- Documentation — the layout and boot protocol are under-documented in-tree; precise docs are genuinely welcome.
gh issue list --repo firecracker-microvm/firecracker \
--search "boot OR e820 OR cmdline OR initrd OR memory layout OR fdt" --state open
Deliverables
You must demonstrate all of the following before advancing to Level 7:
-
A written boot-path reading log tracing
build_microvm_for_bootthrough memory creation, kernel load, boot-param configuration, vCPU register setup, and vCPU thread start, naming the function/file at each step (Lab 6.1). -
An annotated memory-layout diagram for x86_64 with each constant pinned by a
rgagainstlayout.rson your branch, plus the aarch64 contrast (Lab 6.2). -
Empirical observation of boot config effects: changing
boot_args(console=,reboot=k,panic=1, a custom param), adding aninitrd, and reading the resulting cmdline (including thevirtio_mmio.device=advertisements) in the boot log (Lab 6.3). -
From memory: the initial long-mode vCPU state (
rip,rsi,cr0/cr4/efer, GDT, identity page tables) and why each value; and why guest RAM is split into regions around the MMIO gap.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Thinking Firecracker runs a BIOS/bootloader | You look for firmware that isn't there | FC loads the ELF and jumps straight to e_entry in long mode |
| Assuming guest RAM is one contiguous region | You misread the MMIO gap and high RAM | RAM is split around the gap below 4 GiB; large mem spills above 4 GiB |
| Confusing the zero page with the cmdline | You mix the structure with its pointer | The zero page (boot_params) contains a pointer to the cmdline at CMDLINE_START |
| Trusting layout line numbers / constant names | Your rg finds nothing after a refactor | Grep by role; the guide marks these version-sensitive |
Expecting bzImage to load | Boot fails confusingly on x86_64 | FC loads uncompressed vmlinux on x86 (PE Image on aarch64) |
Programming rip before loading the kernel | e_entry is unknown | Load the kernel first; rip = e_entry comes from the ELF parse |
Forgetting rsi = ZERO_PAGE_START | Kernel can't find boot params; hangs | The x86 boot protocol passes boot_params in rsi |
| Reading only x86_64 | You miss the FDT model | aarch64 has no zero page; it uses an FDT in x0 |
How to Verify Success
# 1) You can locate every boot step from a grep chain.
rg -n "fn build_microvm_for_boot" src/vmm/src/builder.rs
rg -n "load_kernel|Elf|e_entry" src/vmm/src/arch/ src/vmm/src/builder.rs
rg -n "ZERO_PAGE_START|CMDLINE_START|HIMEM_START|MMIO_MEM_START" src/vmm/src/arch/x86_64/layout.rs
rg -n "configure_system|boot_params|e820|FdtWriter" src/vmm/src/arch/
# 2) You can boot and read what the guest actually got, from the serial log.
# (Boot a microVM per docs/getting-started.md; capture console=ttyS0 output.)
# Look for: "Command line: ..." and "BIOS-e820: [mem ...]" lines printed by the kernel.
# 3) You can register guest memory by hand (this is rust-vmm Lab r1/r2 territory).
rg -n "set_user_memory_region|KVM_SET_USER_MEMORY" src/vmm/src/vstate/vm.rs
When you can narrate the boot path, draw the memory layout from the constants on your branch, and
predict how a boot_args change shows up in the kernel's printed cmdline, you are ready for the device
model in Level 7.
PR Profile: Level 6 Graduate
A graduate of this level can credibly open these kinds of PRs:
| PR type | Example |
|---|---|
| Docs | Document the x86_64 memory layout / boot protocol precisely in docs/, pinned to the constants in layout.rs. |
| Error messages | Improve the diagnostic when a non-ELF / wrong-format kernel is supplied, or when mem_size interacts badly with the MMIO gap. |
| Validation | Tighten boot-source validation (cmdline length, initrd path, kernel format) with a clear error. |
| Bug fix | A boundary bug in region splitting near the MMIO gap, or an off-by-one in an e820 entry / FDT node. |
| Test | An integration test asserting the guest's reported memory map / cmdline matches the configured machine. |
| aarch64 parity | An FDT node correctness fix, or aligning an aarch64 layout constant with its x86_64 sibling. |
These are real, mergeable contribution shapes — not toy exercises. The boot path is well-trodden but still produces genuine bugs, and precise documentation of it is perennially welcome.
Begin with Lab 6.1: Trace the Boot Sequence. Lean throughout on the boot sequence deep dive, the guest memory management deep dive, the ACPI and MPTable deep dive, and the rust-vmm chapters on linux-loader and vm-memory.