re-org docs
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# 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
|
||||
[danos-file-system-hierarchy-FSH.md](../file-system-development/danos-file-system-hierarchy-FSH.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 FHS path), offset: u64, len: u64
|
||||
blobs... each entry's file bytes, at its offset within the image
|
||||
```
|
||||
|
||||
- **Names are full FHS paths** (`/system/services/init`), not basenames — that
|
||||
is what "v2" means. The 64-byte capacity matches `abi.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.py` rejects them).
|
||||
- **The v1 magic (`"DNRD"`, basename entries) is rejected**, not tolerated: a
|
||||
stale image should fail loudly at `Reader.init`, not misparse names.
|
||||
- `initial_ramdisk.Reader` is the one validated view over the bytes — magic
|
||||
check, table bounds, per-blob bounds — used by the kernel and shared with the
|
||||
loader. `Reader.find` resolves 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 FHS home.
|
||||
Three artifacts are derived from that same list, in the same build graph, so
|
||||
they cannot drift apart:
|
||||
|
||||
1. **The tree**: each binary installed at its FHS path (`zig-out/system/...`
|
||||
and `zig-out/test/...`, mirrored onto the FAT boot volume by
|
||||
`tools/make-fat-image.py`).
|
||||
2. **The manifest** (`system/manifest`): the FHS path of every bundled binary,
|
||||
one per line — the loader's per-file fallback input.
|
||||
3. **The capsule**: `tools/pack-system-image.py` packs the same binaries into
|
||||
the v2 container, installed at `zig-out/boot/system.img` and placed on the
|
||||
boot volume at `boot/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:
|
||||
|
||||
1. **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.
|
||||
2. **The manifest** — read `system\manifest` and 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.
|
||||
3. **The tree walk** — enumerate `/system` and `/test` recursively (`/test`
|
||||
is 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_spawn` looks binaries up in
|
||||
the ramdisk via `Reader.find` — exact FHS 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 `/system` and, 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
|
||||
FHS 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 by
|
||||
`tools/make-fat-image.py` — the thing a machine actually boots, which
|
||||
*contains* `boot/system.img` alongside the loader, kernel, manifest, and
|
||||
tree. See [efi.md](efi.md) and [release-iso.md](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.
|
||||
Reference in New Issue
Block a user