Lab 1: GDB a microVM
Background
There are two programs running when a microVM is live, and they are on opposite sides of the KVM
boundary. One is firecracker — an ordinary (jailed) Linux process running on the host, with its
API thread, its VMM thread, and its vCPU threads. The other is the guest: a Linux kernel and your
rootfs, executing on the physical CPU under hardware virtualization, where the host's view stops at
ioctl(vcpufd, KVM_RUN). A host debugger attached to firecracker can see Vcpu::run and the device
emulation; it cannot see start_kernel or a guest page fault, because that code never runs as host
instructions — KVM runs it directly on the silicon and only hands control back to the VMM on a VM
exit.
So debugging a microVM means choosing a debugger by which side the bug is on:
- To debug the guest (the kernel, a driver, guest userspace), you use Firecracker's
gdbfeature: a GDB stub compiled into the VMM that speaks the GDB Remote Serial Protocol over a Unix socket. Yourgdb, loaded with the guest'svmlinuxsymbols, connects to that socket and drives the guest through KVM. This is the same idea as QEMU's-s -Sgdbserver, or KGDB, but built into Firecracker and using KVM's hardware breakpoints. - To debug the VMM (device emulation, the run loop, the API path, snapshot/restore, a seccomp denial), you attach an ordinary host gdb or lldb to the firecracker PID, exactly as you would any Rust binary.
This lab makes you do both, in one session, against one microVM, so the boundary becomes muscle memory.
Warning: GDB support is a debug-build feature, gated behind a Cargo feature and intended for development, not production. It widens what a connected debugger can do to the guest and is not part of the hardened, seccomp-locked production path. Never enable
gdbon a production binary. (Verify the feature name and the gating on your branch — see Step 1.)
Why This Lab Matters for Contributors
- The single most common wasted afternoon in VMM work is debugging the wrong program — setting a host breakpoint on a guest symbol, or vice versa. Internalizing guest vs VMM here saves that afternoon forever.
- When you contribute to the boot sequence or
guest memory, being able to stop the guest at
start_kerneland inspect its early state is the difference between reasoning about the zero page/e820 from the code and seeing the kernel consume it. - When you contribute to the vCPU run loop or a
virtio device, breaking in the VMM at the exact
VcpuExitor MMIO dispatch lets you watch an emulation decision happen instead of inferring it. - The GDB stub itself is real Firecracker code (
src/vmm/src/gdb/) you could one day contribute to — register coverage, address translation, multi-core support are all live edges.
Prerequisites
- Lab 1.1: Build from source — you can build with
tools/devtool. - Lab 1.3: Boot your first microVM — you can drive the API socket and get a guest login.
- Level 4 — you can read the vCPU run loop.
- A guest
vmlinuxwith debug info (see Step 2),gdbinstalled on the host, and (optional)lldbif you prefer it for the VMM side.
# Verify your toolchain before you start.
cd ~/src/firecracker
which gdb || echo "install gdb"
ls -l /dev/kvm # must be read+write to you
git rev-parse --short HEAD # record the commit you are debugging on
Step 1: Build Firecracker with the gdb feature
The GDB stub is compiled in only when you ask for it. Find the feature and the field it enables — do not trust the names from memory, they are version-sensitive.
cd ~/src/firecracker
# The Cargo feature that pulls in the GDB stub, and the gdbstub crate it uses.
rg -n "^gdb|feature = \"gdb\"|gdbstub" Cargo.toml src/vmm/Cargo.toml src/firecracker/Cargo.toml
# The stub module itself: target.rs (the gdbstub Target impl), event_loop.rs, arch/.
ls src/vmm/src/gdb/
rg -n "gdb_socket_path|GdbServer|target_remote|run_loop|hbreak|HwBreakpoint" src/vmm/src/gdb/
You should find a gdb/ module containing (names verify on your branch) mod.rs, target.rs,
event_loop.rs, and an arch/ subdirectory — Firecracker implements the
gdbstub crate's Target trait over KVM's guest-debug ioctls.
Now build with the feature on. A debug build is what you want for debugging (symbols, no
inlining surprises):
# Build the firecracker binary with the gdb feature enabled (debug profile).
./tools/devtool build --debug -- --features "gdb"
# Confirm the binary exists (arch/profile may differ — find it, don't assume).
find build/cargo_target -type f -name firecracker | rg "debug"
FC=$(find build/cargo_target -type f -name firecracker | rg "debug" | head -1); echo "$FC"
Note:
--features "gdb"is passed throughdevtooltocargo. If the flag plumbing differs on your branch,rg -n "features|gdb" tools/devtoolshows howdevtoolforwards cargo args. The ground truth is alwayscargo build --features "gdb"somewhere underneath.
Step 2: Get a guest kernel with debug info
You cannot set a symbolic breakpoint (hbreak start_kernel) without symbols, and the guest debug
config needs a couple of kernel options. The CI vmlinux images may be stripped; for this lab build
or fetch a vmlinux compiled with at least these options (find the guest configs in-tree):
ls resources/guest_configs/
# The options the GDB workflow needs (verify against docs/gdb-debugging.md on your branch):
rg -n "CONFIG_DEBUG_INFO|CONFIG_FRAME_POINTER|CONFIG_SCHED_MC" resources/guest_configs/
The documented requirements are CONFIG_DEBUG_INFO=y and CONFIG_FRAME_POINTER=y; the docs also
recommend disabling multi-core scheduler heuristics (CONFIG_SCHED_MC=n, CONFIG_SCHED_MC_PRIO=n)
because the stub's multi-core handling is limited. Read the canonical instructions:
sed -n '1,120p' docs/gdb-debugging.md
For the rest of this lab, assume you have ./vmlinux (with symbols) and a rootfs ./rootfs.ext4, as
in Lab 1.3.
Step 3: Boot a microVM with the GDB socket enabled
Enabling the stub is a machine-config field: gdb_socket_path. Set it before
InstanceStart, with a single vCPU (multi-vCPU guest debugging is limited). Two ways:
Config-file boot (simplest — the whole machine in one JSON):
cat > vm.json <<'EOF'
{
"boot-source": {
"kernel_image_path": "./vmlinux",
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off nomodule"
},
"drives": [
{"drive_id": "rootfs", "path_on_host": "./rootfs.ext4",
"is_root_device": true, "is_read_only": false}
],
"machine-config": {
"vcpu_count": 1,
"mem_size_mib": 256,
"gdb_socket_path": "/tmp/gdb.socket"
}
}
EOF
# Start the gdb-enabled binary. It will set up the VM, then BLOCK waiting for gdb
# to connect on /tmp/gdb.socket before the guest runs a single instruction.
sudo "$FC" --no-api --config-file vm.json
API boot (set gdb_socket_path via PATCH /machine-config pre-boot, then InstanceStart):
API=/tmp/fc.sock
sudo "$FC" --api-sock "$API" &
curl -X PUT --unix-socket "$API" --data \
'{"kernel_image_path":"./vmlinux","boot_args":"console=ttyS0 reboot=k panic=1 pci=off nomodule"}' \
http://localhost/boot-source
curl -X PUT --unix-socket "$API" --data \
'{"drive_id":"rootfs","path_on_host":"./rootfs.ext4","is_root_device":true,"is_read_only":false}' \
http://localhost/drives/rootfs
# The key line: enable the gdb socket on machine-config, pre-boot.
curl -X PATCH --unix-socket "$API" --data \
'{"gdb_socket_path":"/tmp/gdb.socket"}' http://localhost/machine-config
curl -X PUT --unix-socket "$API" --data \
'{"action_type":"InstanceStart"}' http://localhost/actions
# InstanceStart returns, but the guest is HALTED at entry, waiting for gdb.
Note: With the GDB socket set, Firecracker stops the guest at the kernel entry point and waits for a debugger — like QEMU's
-S. Nothing in the guest runs until youcontinuefrom gdb. That is exactly what you want to break atstart_kernel.
Step 4: Attach GDB to the GUEST and break in the kernel
In a second terminal, start gdb on the guest vmlinux and connect to the socket. The connection is a
Unix socket (the GDB remote protocol over a UDS):
gdb ./vmlinux
(gdb) target remote /tmp/gdb.socket
Remote debugging using /tmp/gdb.socket
0x000000000... in ?? ()
# Hardware breakpoint at the kernel's C entry. Use hbreak, not break --
# software breakpoints rewrite guest memory, which is fragile this early;
# KVM hardware breakpoints are what the stub supports cleanly.
(gdb) hbreak start_kernel
Hardware assisted breakpoint 1 at 0xffffffff...: file init/main.c, line ...
(gdb) continue
Continuing.
Breakpoint 1, start_kernel () at init/main.c:...
...
(gdb) backtrace
(gdb) info registers rip rsp rsi # rsi held the zero-page address at boot
(gdb) print boot_command_line # the kernel cmdline you passed
You are now stopped inside the guest kernel, on real virtualized hardware, with full symbols. Things to do here to prove the boundary to yourself:
# Step a few instructions of guest code.
(gdb) stepi
(gdb) stepi
# Break later, in device setup, to watch a virtio-mmio probe.
(gdb) hbreak setup_arch
(gdb) continue
# Pause a running guest at any time with Ctrl+C (it interrupts vCPU 1),
# then 'continue' to resume. 'kill' / the documented exit ends the session.
Tip: Only a limited subset of CPU registers is exposed by the stub, and on aarch64 guest virtual-address translation is limited (4 KiB pages, not all physical address sizes). If
info registersor reading a guest pointer behaves oddly, that is a known stub limitation, not your mistake —rg -n "registers|translate|read_addr" src/vmm/src/gdb/arch/shows what is implemented.
Step 5: Now debug the VMM — host gdb/lldb on the firecracker process
Detach from the guest (or leave it paused) and switch sides. The VMM is an ordinary host process. Find its PID and the threads.
# Find the firecracker process and its threads (API, VMM, one per vCPU).
FCPID=$(pgrep -n firecracker); echo "$FCPID"
ps -L -p "$FCPID" -o tid,comm # the thread names: fc_api, fc_vmm, fc_vcpu N
Attach host gdb (or lldb -p "$FCPID"). Because Firecracker is seccomp-filtered, attaching may be
blocked by the production filter; for VMM debugging start the binary with --no-seccomp (development
only — it changes the security posture, so never draw production conclusions from a --no-seccomp
run):
# (Re)start for VMM debugging with seccomp off so ptrace/gdb can attach cleanly.
# sudo "$FC" --no-api --config-file vm.json --no-seccomp (dev only)
sudo gdb -p "$FCPID"
Set a breakpoint in the run loop — the host code, not the guest. Locate it first; the symbol path is version-sensitive:
# Find the vCPU run loop and the VM-exit dispatch in the VMM.
rg -n "fn run\b|KVM_RUN|VcpuExit::|fn run_emulation|handle_mmio|fn process" \
src/vmm/src/vstate/vcpu/
# Break where the vCPU loop dispatches a VM exit. Tab-complete the mangled Rust path;
# the role is "the function that matches on VcpuExit". Example shape:
(gdb) break vmm::vstate::vcpu::*::run_emulation
(gdb) continue
# ... trigger guest I/O (e.g. log in, run `ls`) ...
Thread 4 "fc_vcpu 0" hit Breakpoint 1, ... run_emulation (...)
(gdb) bt
(gdb) info threads # see fc_vmm and the fc_vcpu threads
(gdb) thread apply all bt # a "where is every thread" snapshot — the VMM analogue of a thread dump
To break in the VMM thread (device emulation, the EventManager loop) instead of a vCPU thread:
rg -n "fn process|fn handle_event|run_event_loop|EventManager|fn write\b|fn read\b" \
src/vmm/src/devices/virtio/block/ src/vmm/src/devices/virtio/mmio.rs
(gdb) break vmm::devices::virtio::block::*::process_queue
(gdb) continue
# ... do a guest disk read ... then inspect the request, the descriptor chain, guest memory.
This is the payoff: the same microVM, debugged from both sides. The guest gdb stopped you at
start_kernel; host gdb stops you at the VMM code that emulates the hardware the guest is talking
to.
Step 6: Tell the two debuggers apart, deliberately
Run the experiment that cements the boundary. With the guest gdb attached:
(gdb) break run_emulation # a VMM symbol
Function "run_emulation" not defined. # the guest has no idea what that is
With host gdb attached to firecracker:
(gdb) break start_kernel # a guest symbol
Function "start_kernel" not defined. # the VMM binary has no guest kernel symbols
Each debugger only knows its own program. That single fact — that start_kernel and run_emulation
live in different symbol tables on different sides of KVM_RUN — is the whole lab.
Implementation Requirements / Deliverables
-
A
gdb-enabled debug build of firecracker, located withfind(not a memorized path). -
A microVM booted with
gdb_socket_pathset, halted at guest entry. -
A guest gdb session that hits
hbreak start_kerneland prints a backtrace +boot_command_line. -
A host gdb/lldb session that breaks in the vCPU run loop (a
VcpuExitdispatch) and in a VMM-thread device path, withthread apply all btcaptured once. -
A one-paragraph write-up: for three concrete symptoms (a guest kernel panic, a block-read that
returns wrong bytes, a VMM crash on
InstanceStart), state which debugger you would reach for and why.
Troubleshooting
target remote /tmp/gdb.socket hangs or "Connection refused"
The socket only exists once Firecracker has set up the VM and is waiting. Confirm the binary was built
with --features "gdb" (strings "$FC" | rg gdb or just re-run Step 1's rg), that
gdb_socket_path was set before InstanceStart, and that the path matches exactly. ls -l /tmp/gdb.socket should show a socket.
hbreak start_kernel says "Function not defined"
Your vmlinux has no symbols, or you loaded the wrong file. gdb ./vmlinux then info functions start_kernel. If empty, rebuild the guest kernel with CONFIG_DEBUG_INFO=y (Step 2). A stripped CI
kernel will not work for symbolic breakpoints.
The breakpoint is hit but registers/memory look wrong
Expected within limits: the stub exposes a subset of registers and limited address translation
(especially aarch64). Check what src/vmm/src/gdb/arch/ actually implements; do not assume full
register coverage. For multi-vCPU oddities, rebuild the guest with CONFIG_SCHED_MC=n and use a
single vCPU.
Host gdb can't attach to firecracker ("Operation not permitted")
Either seccomp is blocking the ptrace path (start the dev run with --no-seccomp), or
ptrace_scope is locked down on your host (cat /proc/sys/kernel/yama/ptrace_scope; sudo the gdb,
or run firecracker and gdb as the same user). Remember --no-seccomp changes the posture — re-verify
any fix under the real filter.
Rust symbol names won't tab-complete
They are mangled and the module path moves. Find the function with rg first (Steps 5), then in gdb
use rbreak with a regex on the role, e.g. rbreak run_emulation, and pick from the list.
Expected Output
A guest session that looks like:
(gdb) target remote /tmp/gdb.socket
(gdb) hbreak start_kernel
(gdb) c
Breakpoint 1, start_kernel () at init/main.c:...
(gdb) bt
#0 start_kernel () at init/main.c:...
and a host session where info threads shows fc_vmm plus one fc_vcpu N per configured vCPU, and a
breakpoint in the run loop fires when the guest does I/O.
Stretch Goals
- Watch a virtio-block request cross the boundary. Set a guest breakpoint in the block driver's
submit path and a host breakpoint in
process_queue. Trigger one read; step the request from the guest driver, through the kick, to the VMM's descriptor-chain handling. You have now watched one request traverse the KVM boundary in both debuggers. - Read the GDB stub's
Targetimpl. Opensrc/vmm/src/gdb/target.rsand map thegdbstubTargetmethods (read_registers,write_addr, breakpoint ops) onto the KVM guest-debug ioctls they call. Note exactly which registers are exposed and which are stubbed — that is a real contribution surface. - Break on a guest page fault. Set a watchpoint or a breakpoint in the guest fault path and a
host breakpoint where guest memory is mapped (
rg -n "GuestMemoryMmap|fn get_slice|MAP_" src/vmm/src/vstate/memory.rs) to see the same physical page from both sides.
Validation / Self-check
Answer without notes; these gate completion:
- Why can a host gdb attached to firecracker never set a working breakpoint on
start_kernel, and why can the guest gdb never break onrun_emulation? - What does
gdb_socket_pathgo under in the configuration, and when (relative toInstanceStart) must it be set? - Why does the workflow use
hbreakrather thanbreakfor the guest kernel, and what KVM feature makeshbreakpossible? - Name two documented limitations of the GDB stub and where in
src/vmm/src/gdb/you would confirm them. - Why is
--no-seccompsometimes needed for the VMM side, and why must you never report a finding from a--no-seccomprun as a production conclusion? - Given "the guest hangs with no console output after
InstanceStart," which debugger do you reach for first, and what is the very first breakpoint you set? - In host gdb, what is the VMM analogue of a thread dump, and what does it tell you that a single backtrace does not?
Next: Lab 2: Tracing and metrics — when you should not reach for a debugger at all, and how to diagnose a failing boot from counters and logs without attaching to anything.