6.6 KiB
system.img — the boot capsule
What it is
boot/system.img is the boot capsule: every bundled user binary — init, the
services, the drivers, the test programs — packed into one file on the boot
volume. It is not a filesystem image and it is not compressed; it is exactly the
kernel's initial-ramdisk wire format (system/initial-ramdisk.zig, format
v2), written to disk ahead of time. The EFI loader reads it in a single
sequential pass and hands the bytes to the kernel unmodified.
The capsule is a performance artifact, not a source of truth. The boot
volume's /system and /test file trees remain the canonical layout (see
file-system-hierarchy.md);
the capsule is a pre-baked snapshot of the same binaries, derived from the same
build graph, so the running system is identical whether the loader read the
capsule or walked the tree.
Why it exists
Firmware file I/O has exactly one fast shape: one open + one sequential read. Everything else is a lottery. Loading the system per-file — dozens of opens, seeks, and short reads through the firmware's FAT driver — measured minutes on real hardware, against milliseconds in QEMU/OVMF. Packing the binaries into a single file turns the whole of user space into the shape firmware is good at.
Because the capsule already is the ramdisk wire format, the loader doesn't
even repack it: loadCapsule (boot/efi.zig) validates the magic and passes
the buffer straight through as BootInformation.initial_ramdisk_base/len.
The format
The container is deliberately trivial — danos owns both producer and consumer, so it need be no fancier. Little-endian throughout:
Header magic: u32 = "DNR2" (0x32524E44), count: u32
Entry × count name: [64]u8 (NUL-padded hierarchy path), offset: u64, len: u64
blobs... each entry's file bytes, at its offset within the image
- Names are full hierarchy paths (
/system/services/init), not basenames — that is what "v2" means. The 64-byte capacity matchesabi.maximum_process_name, so a task named after its binary path is never truncated. Paths longer than 63 bytes are a build error (pack-system-image.pyrejects them). - The v1 magic (
"DNRD", basename entries) is rejected, not tolerated: a stale image should fail loudly atReader.init, not misparse names. initial_ramdisk.Readeris the one validated view over the bytes — magic check, table bounds, per-blob bounds — used by the kernel and shared with the loader.Reader.findresolves a binary by exact path first, then by unique basename, ASCII case-insensitively (the entries come from a FAT volume, whose name lookups are case-insensitive by definition).
How it is built
build.zig maintains one bundled list — every user binary and its hierarchy home.
Three artifacts are derived from that same list, in the same build graph, so
they cannot drift apart:
- The tree: each binary installed at its hierarchy path (
zig-out/system/...andzig-out/test/..., mirrored onto the FAT boot volume bytools/make-fat-image.py). - The manifest (
system/manifest): the hierarchy path of every bundled binary, one per line — the loader's per-file fallback input. - The capsule:
tools/pack-system-image.pypacks the same binaries into the v2 container, installed atzig-out/boot/system.imgand placed on the boot volume atboot/system.img.
Note what the capsule does not contain: the kernel (system/kernel is loaded
separately by loadKernel, as an ELF) and the EFI loader itself. It is user
space only.
How it is loaded
loadSystemTree (boot/efi.zig) tries three strategies, most portable first —
the running system cannot tell which one ran, because all three produce the
same in-RAM ramdisk image:
- The capsule — open
boot\system.img, read it whole, check the magic, hand it over as-is. The normal path on any build-produced volume. - The manifest — read
system\manifestand open each listed path by name. FAT name lookup is case-insensitive and firmware-portable, unlike directory enumeration. The loader assembles the v2 image in RAM itself. - The tree walk — enumerate
/systemand/testrecursively (/testis optional: a stick without fixtures still boots). Last resort for hand-assembled sticks with neither file: some firmware FAT drivers return bare 8.3 names uppercase from enumeration, which is why this is the fallback and not the primary path.
All three are best-effort: a kernel-only volume still boots — the kernel just has no user binaries to spawn and reports the absence.
One operational consequence of the ordering: the capsule shadows the tree.
If you hand-edit binaries on a stick that also carries a boot/system.img,
your edits are invisible — the loader boots the capsule's snapshot. Delete
boot/system.img from the volume to force the manifest/tree path.
What the kernel does with it
The loader records the image's physical base and length in BootInformation;
the kernel (kernel.zig) then publishes the same bytes twice, to two
consumers:
- The process layer (
process.zig):system_spawnlooks binaries up in the ramdisk viaReader.find— exact hierarchy path, or unique basename for pre-path callers — and loads them as fresh ring-3 processes. The stored path becomes the task's name. - The VFS root (
vfs.zig,setInitialRamdisk): the image is mounted as kernel-backed, read-only mounts — one per top-level tree named by the entry paths, so/systemand, when the fixtures are bundled,/test. Directory nodes are derived from the entry paths (the unique parents), so the trees are listable and their files readable over the normal VFS protocol — the boot tree every process sees comes straight out of the capsule bytes.
The image is never copied after the handoff and never mutated: the initrd is immutable, which is what makes the VFS's node serving lock-free.
What it is not
- Not
danos-usb.img. That is the 64 MiB FAT32 boot volume built bytools/make-fat-image.py— the thing a machine actually boots, which containsboot/system.imgalongside the loader, kernel, manifest, and tree. See efi.md and release-iso.md. - Not a mountable filesystem. No FAT, no block device, no driver — just a
header, a table, and concatenated blobs, parsed by ~90 lines of
initial-ramdisk.zig. - Not required. It is the fast path, with two slower equivalents behind it.
- Not a place where state lives. It is regenerated on every build from the bundled binaries; nothing writes to it, at build time or runtime.