6 — Booting: firmware to first prompt
Requirements: chapters 3, 4 and 5, with EXT4_FS, VIRTIO_BLK, VIRTIO_PCI and DEVTMPFS confirmed in your config.
The hand-offs
A PC boot is a series of hand-offs:
- Firmware loads something. With
CONFIG_EFI_STUB, the kernel poses as an EFI program, so firmware can run it directly (EFI stub). - The kernel starts, and unpacks the initramfs, a cpio archive in
newcformat (format). /initin the initramfs finds the real root and switches to it.- The real init starts everything else.
-kernel in QEMU skips firmware entirely. And dmesg starts when kernel timekeeping starts, so it can’t tell you how long firmware took.
Decision: an initramfs always, even when the root is simple. It’s the only place to put logic that must run before the root exists, such as verity, encryption or a network root.
A Unified Kernel Image bundles a stub, the kernel, an initramfs and a command line in one EFI file you can sign as a unit (UKI spec). ukify builds them (ukify(1)).
Version note: since Linux 7.0, a new empty filesystem called nullfs sits underneath rootfs, and the docs prefer pivot_root for leaving the initramfs (kernel docs). Older explanations of why pivot_root can’t work from an initramfs are out of date on 7.0 and later. This walkthrough uses switch_root, which still works everywhere.
Hands-on
Step 1: build a root disk
mkfs.ext4 -d fills the filesystem without needing root:
cd /lo/out
mkdir -p /lo/rootfs/proc /lo/rootfs/sys /lo/rootfs/dev /lo/rootfs/sbin
cp init /lo/rootfs/sbin/init
truncate -s 512M root.ext4
mkfs.ext4 -d /lo/rootfs root.ext4
Expected: dumpe2fs -h root.ext4 | grep -i 'filesystem state' reports clean.
Step 2: write the initramfs /init
rootwait won’t help here, because it only applies to the kernel’s own root mounting. In your own /init, you write the retry loop yourself, which is the better lesson:
cat > ramfs/init <<'E'
#!/bin/sh
export PATH=/bin
mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t devtmpfs dev /dev
n=0
while [ ! -b /dev/vda ] && [ $n -lt 50 ]; do sleep 0.1; n=$((n+1)); done
if ! mount -o ro /dev/vda /mnt; then
echo "root not found after $n tries; rescue shell"
exec sh
fi
mount --move /dev /mnt/dev
umount /proc /sys
exec switch_root /mnt /sbin/init
E
chmod +x ramfs/init
mkdir -p ramfs/mnt
for a in mount umount sleep switch_root; do ln -sf busybox ramfs/bin/$a; done
( cd ramfs && find . | cpio -o -H newc ) | gzip > initramfs.cpio.gz
exec matters, because switch_root must run as PID 1. Devtmpfs creates the device nodes, so if you’re waiting, it’s for the device to appear, not for a missing node.
Step 3: boot it
qemu-system-x86_64 -m 512M -nographic -no-reboot \
-kernel bzImage -initrd initramfs.cpio.gz \
-drive file=root.ext4,format=raw,if=virtio \
-append "console=ttyS0 panic=-1"
Expected: your chapter 4 init runs from disk, and cat /proc/mounts shows the virtio device as /.
Step 4: make it fail on purpose
Boot again without the -drive line.
Expected: after about five seconds, you get the rescue message and a shell, with no panic. That’s the failure path working.
Step 5: draw the timeline
Add a line to each stage that prints the time from /proc/uptime. Then draw three bars: kernel to /init, /init to real init, and real init to shell. These are your numbers.
Step 6: try the 7.0+ method
Replace switch_root following the three calls in the kernel docs:
chdir(new_root);
pivot_root(".", ".");
umount2(".", MNT_DETACH);
This only applies on Linux 7.0 and later.
If it fails
switch_rootcan’t exec: the init path in/mntis wrong, or the binary isn’t static.- No
/dev/vda:VIRTIO_BLKorVIRTIO_PCIis missing, or you left outif=virtio. - Panic right after
/initexits: a script path fell through. Always end inexec. switch_rootprints usage: it isn’t PID 1, so you forgotexec.sleep 0.1fails: BusyBox was built without fractional sleep. Usesleep 1and a smaller limit.
Principle: startup is a dependency graph, not a script
Boot looks like a sequence because that’s how it’s written down. It is actually a graph: root needs the block device, which needs the driver, which needs the bus probe; the console needs the tty driver; a network root needs the NIC and DHCP and a clock. Writing it as a script hides the edges, and hidden edges are where boot hangs live. systemd’s units, Kubernetes’ readiness probes and your deployment pipeline are all attempts to make the graph explicit.