Chapter 6: Boot§
After this chapter you can trace the whole path from power button to first userspace process: reset vector, firmware, UEFI boot entries, the Secure Boot signature chain, bootloader, kernel decompression, initramfs, and start_kernel handing off to PID 1. You will build an initial ramdisk from scratch, boot the kernel you compiled in Chapter 0 inside QEMU, then deliberately break the boot and read the panic like a stack trace instead of a wall of noise.
Boot is the layer where none of your usual tools are running yet. No shell, no logging daemon, and until late in the sequence no userspace at all, so the instrument you actually have is your ability to name which stage failed from the last line printed. If you are heading for kernel work, this chapter is the ground every later chapter stands on: page tables, the scheduler, and PID 1 all get built here, in order, before anything else exists. If you run machines that have to come up unattended, the same skill is the difference between a ten-minute fix and a lost afternoon in front of a black screen.
The problem [fundamental]§
An operating system is just a program. Programs are loaded from disk into memory by an operating system. So who loads the operating system?
That circularity is the boot problem, and it is worse than it first looks. To read the kernel off an NVMe drive you need an NVMe driver, which lives inside the kernel you have not loaded yet. To find the kernel file you need to understand a filesystem, implemented in the kernel. And at power-on, even RAM does not work until the memory controller is configured. A freshly powered CPU knows exactly one thing: the address of its first instruction.
There is a second problem stacked on top: trust. Every boot stage runs with total control of the machine, before any OS protections exist. If an attacker replaces an early stage, they own the machine below the operating system, invisibly. So boot is a chain of stages. Each stage is small enough to be loaded by the stage before it, and on modern machines each is cryptographically verified by the stage before it.
Boot is bootstrapping, literally: progressively smarter programs, each pulling the next one up.
Unfolded [fundamental]§
Power-on: the reset vector [fundamental]§
When an x86 CPU comes out of reset, the hardware forces the instruction pointer to a fixed address: 0xFFFFFFF0, sixteen bytes below the 4 GiB mark. This is the reset vector. The CPU starts in a mode that behaves like the 16-bit "real mode" of the original 8086 (no memory protection, no virtual memory) with one quirk: the code segment's hidden base register is preset so fetches actually hit that top-of-memory address. The motherboard maps a flash chip containing the firmware exactly there, so the first instruction ever executed is factory-burned firmware code. Sixteen bytes is only room for a jump into the firmware proper. That is exactly what sits there.
This is the answer to the chicken-and-egg problem because it removes all choice. The chain of "who loads whom" has to bottom out somewhere, and it bottoms out in a hardwired address and a ROM chip.
Firmware's job [fundamental]§
First, make the machine usable: initialize and calibrate the DRAM controller (until then, code runs from flash, with the CPU cache pressed into service as scratch memory), train interconnects, enumerate PCIe devices, apply microcode updates, build the tables describing the hardware. Second, boot policy: pick a device, find the next stage, optionally verify its signature, load it, jump. How that second task works is what split the industry into two eras.
Legacy BIOS vs UEFI [fundamental]§
The legacy BIOS scheme, inherited from the 1981 IBM PC, is brutally simple. Read the first 512-byte sector of the boot disk, the Master Boot Record (MBR), into memory at address 0x7C00. Check that its last two bytes are the signature 0x55 0xAA. Jump to it in 16-bit real mode. That is the entire contract.
Everything wrong with it follows: 512 bytes forced fragile multi-stage loaders hidden in disk gaps; MBR partitioning tops out at four primary partitions and 2 TiB disks; the BIOS understands no filesystem; there was no standard multi-OS menu; and nothing verified anything, so boot-sector viruses were a genre.
UEFI (Unified Extensible Firmware Interface) replaced the idea itself: the firmware understands a partition table and a filesystem, and launches an ordinary executable file.
| Legacy BIOS | UEFI | |
|---|---|---|
| First OS-side code | 512-byte MBR loaded at 0x7C00 | a PE/COFF .efi executable, loaded from a file |
| Partition scheme | MBR: 4 primary partitions, 2 TiB limit | GPT: GUID-typed partitions, redundant headers |
| Filesystem awareness | none | FAT driver mandated by spec |
| CPU state at handoff | 16-bit real mode | native mode (64-bit long mode on x86_64) |
| Boot configuration | device order only | named entries in NVRAM, priority-ordered |
| Signature verification | none | Secure Boot |
| Multi-OS story | loaders fighting over one MBR | a boot manager listing many entries |
UEFI won because firmware vendors, Microsoft, and OEMs all needed the same things at once: disks past 2 TiB (GPT), real executables instead of sector-smuggled code, and a malware answer (Secure Boot). Since roughly the Windows 8 hardware generation (2012), UEFI is the default on nearly every x86 machine; the legacy path survives only as a compatibility mode.
The UEFI vocabulary you need downstream [working]§
GPT (GUID Partition Table). The modern partition table: every partition gets a unique ID and a type GUID, the table is stored redundantly at both ends of the disk with checksums, and the four-partition limit is gone.
The EFI System Partition (ESP). One FAT-formatted partition (the spec allows FAT12/16/32) with a special type GUID, holding boot executables in paths like \EFI\fedora\shimx64.efi. Linux mounts it at /boot/efi or /efi. The firmware can read it because a FAT driver is built into the firmware.
EFI executables. What the firmware runs: PE/COFF binaries. That is the same container format as Windows .exe, an artifact of UEFI's Intel/Microsoft lineage. Bootloaders are EFI executables, and so is the Linux kernel itself when you want it to be: with CONFIG_EFI_STUB, the kernel image carries a PE/COFF header and the firmware can execute it directly.
Boot entries in NVRAM. The firmware keeps variables in non-volatile RAM: Boot0000, Boot0001, … each naming an EFI executable on some partition, plus BootOrder listing which to try in what order. From Linux you edit these with efibootmgr. With no usable entry, firmware falls back to a well-known path (\EFI\BOOT\BOOTX64.EFI on x86_64), which is why USB installers boot anywhere without registering anything.
Disk (GPT)
┌────────────────────────────────────────────────────────┐
│ ESP (FAT32) │ / (ext4/btrfs/…) │ swap │
│ \EFI\BOOT\BOOTX64.EFI │ /boot/vmlinuz-… │ │
│ \EFI\coconut\shimx64.efi│ /boot/initramfs-… │ │
│ \EFI\coconut\grubx64.efi│ │ │
└────────────────────────────────────────────────────────┘
▲ firmware reads this ▲ bootloader/kernel read these
Secure Boot, unfolded [working]§
Secure Boot is UEFI's answer to "how do I know the thing I'm about to execute is legitimate?" The firmware refuses to launch any EFI executable whose signature does not chain to a trusted key. The trust store is a small hierarchy of variables:
| Variable | Name | Role |
|---|---|---|
| PK | Platform Key | The root. One key, held by whoever "owns" the platform (in practice the OEM). Authorizes changes to KEK. |
| KEK | Key Exchange Key(s) | Middle tier. Authorizes updates to db and dbx. Typically the OEM's and Microsoft's keys. |
| db | Signature database | Allowlist: certificates (or raw hashes) of images allowed to run. |
| dbx | Forbidden database | Denylist: revoked certificates and hashes. dbx always wins over db. |
When the firmware loads an executable, it checks that the signature chains to something in db and that neither the certificate nor the binary's hash appears in dbx.
Here is the political part. Practically every PC ships with Microsoft's certificates in db. One is a CA for Windows' own boot chain. The second is the Microsoft Corporation UEFI CA, the "third-party CA," used to sign other people's boot software. Getting your own key into every OEM's firmware is commercially impossible for a Linux distro, so the industry converged on a tiny first-stage bootloader called shim:
UEFI firmware
db contains: Microsoft 3rd-party UEFI CA
│ verifies signature of
▼
shimx64.efi ← submitted for review, signed by Microsoft's CA
embeds: the distro's own certificate (+ user MOKs)
│ verifies signature of
▼
grubx64.efi (or systemd-boot) ← signed by the distro's key
│ verifies signature of
▼
vmlinuz ← signed by the distro's key
│ verifies signatures of
▼
kernel modules ← distro key or a MOK (e.g., DKMS-built NVIDIA)
Each distro writes a shim, submits it through the public shim-review process, and gets that one small binary signed by Microsoft's third-party CA. Shim then extends the chain with the distro's own certificate, compiled in, and uses it to verify the second-stage bootloader and kernel. The distro can now update GRUB and the kernel at will, signing with its own key, without going back to Microsoft.
MOK (Machine Owner Key) closes the last gap, the one you hit the moment you build a kernel or a module. Shim keeps an extra user-controlled key database. You stage a certificate with mokutil --import mycert.der; on next boot, shim's companion MokManager runs before the OS, asks for the password you set, and enrolls the key. From then on, anything signed with it (a custom kernel, a DKMS-built NVIDIA module) passes verification. This is the blue dialog every Linux user meets the first time they install NVIDIA drivers with Secure Boot on.
Revocation got its own mechanism, SBAT: rather than stuffing every vulnerable binary's hash into a finite dbx, boot components carry a generation number in an .sbat section, and shim refuses anything below the current minimum.
One time-sensitive footnote, current as of mid-2026: the Microsoft certificates anchoring this scheme were issued in 2011 with 15-year lifetimes, and the third-party UEFI CA 2011 expired in June 2026; replacement 2023-series CAs have been rolling out via firmware updates, and new signings use them. Expiry does not un-trust already-signed binaries. Secure Boot does not check wall-clock validity, so an old shim keeps booting while its CA sits in db and its hash stays out of dbx. New distros still target the 2023 CA from the start.
If that section took two passes, that is the normal shape of it. The mechanism is four variables and a signature check. What makes it hard is the commercial arrangement bolted on top, and nobody writes that arrangement down in one place, so people who have shipped signed bootloaders still keep the PK/KEK/db/dbx table open in a tab while they work.
Bootloader to kernel [working]§
The firmware has now verified and launched a bootloader. Two dominate on Linux:
| GRUB 2 | systemd-boot | |
|---|---|---|
| Firmware support | legacy BIOS and UEFI | UEFI only |
| What it can load | kernels directly; understands ext4, btrfs, LVM, LUKS, … | EFI executables only, so the kernel must have the EFI stub |
| Configuration | grub.cfg, a full scripting language | one small text file per boot entry |
| Design stance | maximal: themes, modules, network boot | deliberately minimal menu |
GRUB is the default nearly everywhere because it handles every weird case. systemd-boot is a menu that launches EFI executables and nothing more, leaning on the kernel's own EFI stub. That tells you how thin the "bootloader" layer has become on UEFI.
Either way the job is the same: copy the kernel image and initramfs into memory, record the kernel command line (root=…, console=…), and jump to the kernel's entry point following a documented contract, the Linux/x86 boot protocol.
The x86 kernel image is a bzImage, and it nests the same way the boot chain does: a small real-mode setup block (with a header struct the bootloader fills in, the ABI between loader and kernel), a stub of uncompressed code, and the real kernel (vmlinux) compressed inside. The stub sets up protected mode, then 64-bit long mode with identity-mapped page tables, and runs extract_kernel() to decompress vmlinux to its final address. That is the moment a booting machine prints a line like Decompressing Linux... Parsing ELF... done. Booting the kernel. It then jumps to the decompressed kernel's entry point, startup_64, which builds real page tables; finally C code calls the architecture-independent start_kernel(). From here, boot stops being an x86 story and becomes a Linux story.
The initramfs: a root filesystem from nowhere [working]§
The kernel is running, but mounting the real root may require an NVMe driver, RAID or LVM assembly, a LUKS passphrase, or network storage. Those drivers may be modules, living in /lib/modules on the root filesystem it cannot mount yet. Circularity again.
The fix is the initramfs (initial RAM filesystem): a miniature root filesystem shipped as an archive alongside the kernel. Concretely it is a cpio archive, one of Unix's oldest formats and simpler than tar, usually gzip- or zstd-compressed. The bootloader loads it into RAM; early in boot the kernel unpacks it into rootfs, a built-in RAM-backed filesystem that is always the true root of the mount tree. If the unpacked archive contains an executable /init, the kernel runs it as the first process. No disk, no storage drivers. Userspace is running anyway.
That /init (on real distros, a script assembled by dracut or mkinitcpio, bundling busybox or systemd plus exactly the needed modules) does the situation-specific work: load drivers, assemble volumes, prompt for passphrases, mount the real root. Then switch_root deletes rootfs's contents to reclaim RAM, moves the real root's mount to /, and execs the real init system. (A detail error messages mention: in 6.12, the older pivot_root mechanism does not work on rootfs. Rootfs cannot be unmounted, which is exactly why switch_root exists. Kernels newer than 6.12 rework this: mainline now mounts a hidden immutable root beneath rootfs, so pivot_root and unmounting the initramfs work again. switch_root remains the conventional tool.)
start_kernel to PID 1 [working]§
start_kernel() in init/main.c is a long sequence of subsystem initializations (memory management, scheduler, interrupts, timers, console) whose final act is calling rest_init(). That function creates exactly two things: a thread running kernel_init(), which becomes PID 1, and kthreadd, parent of all kernel threads, PID 2. The boot CPU's original thread retires into the idle loop as PID 0. Every process you will ever see descends from PID 1; every kernel thread from PID 2.
kernel_init() finishes driver initialization, waits for the initramfs to unpack, then tries to leave the kernel for userspace in strict order: the initramfs /init if present (overridable with rdinit=); otherwise mount the real root and try init= if given; then the compiled-in default; then /sbin/init, /etc/init, /bin/init, /bin/sh. If everything fails, the kernel panics. There is nothing left to run.
The survivor (systemd on most distros) inherits real obligations, because the kernel does none of this for you:
- Mounts:
/proc,/sys,/dev,/rundo not mount themselves. - Services: start, supervise, and restart everything from udev to sshd, in dependency order.
- Reaping: orphaned processes are re-parented to PID 1, which must
wait()on them forever or zombies accumulate in the process table. - Not dying: PID 1 ignores signals it has no handler for; if it exits anyway, the kernel panics.
The full modern x86_64 chain:
power on
→ CPU at reset vector 0xFFFFFFF0 (firmware ROM)
→ firmware: DRAM init, PCIe enumeration, boot policy
→ UEFI boot manager: NVRAM BootOrder → pick entry on ESP
→ shim (verified via Microsoft 3rd-party CA in db)
→ GRUB / systemd-boot (verified via distro cert in shim)
→ bzImage: real-mode setup → decompress → startup_64
→ start_kernel() ... rest_init()
→ kernel_init(): unpack initramfs, exec /init ← PID 1 exists
→ /init: load drivers, mount real root, switch_root
→ exec real init (systemd today; coconutd in Coconut OS)
→ services, login, you
ARM64: same movie, different opening scene [working]§
ARM64 has no legacy BIOS era to stay compatible with, so there is no 0x7C00, no MBR, no real mode. Each vendor's chip has its own boot ROM and early loader chain (often TF-A plus U-Boot). The kernel's entry contract, documented in the kernel tree, is direct: place the kernel Image in RAM and jump to it with register x0 holding the physical address of a device tree blob (DTB).
The device tree is the big conceptual difference. x86 hardware is discoverable: probe PCIe, read ACPI tables. Much ARM hardware is not. On a phone SoC, nothing enumerates "there is a UART at address X on clock Y." A device tree is a static data structure (source .dts, compiled .dtb) that declares the hardware, and the kernel believes it. ARM servers deliberately imitate the PC instead: UEFI plus ACPI (the Arm SystemReady / SBBR standards), so one generic distro image boots on any compliant box. Either way the description must cover the essentials. The kernel's boot document requires, for instance, that the interrupt controller be fully described. Coconut v1.1's ARM64 server target rides entirely on that UEFI+ACPI convention.
Apple Silicon Macs are the extreme case: Apple's chain (SecureROM in the die, then iBoot) boots only Apple-signed payloads and provides neither UEFI nor ACPI nor a Linux-ready device tree. Asahi Linux bridges this with m1n1, a bootloader that registers with iBoot as if it were an OS; m1n1 sets up the hardware, supplies a device tree, and chainloads U-Boot, which presents a standard UEFI environment so a normal bootloader and kernel can take over. Five stages of adapter between Apple's world and the ARM64 contract above. This is what Coconut v1.2's Apple Silicon stretch goal would inherit, and why it is explicitly conditional on Asahi's velocity.
This whole subsection is background if you are working on x86_64. You can skip it and lose nothing in this chapter's lab, which is x86_64 throughout; come back when you touch an ARM board.
The real thing in Linux [working]§
Where each stage lives in the 6.12-era tree Coconut forks:
| Path | What it is |
|---|---|
Documentation/arch/x86/boot.rst | the boot protocol: header fields a bootloader must fill |
arch/x86/boot/header.S | the real-mode setup header, the loader↔kernel ABI |
arch/x86/boot/compressed/head_64.S | the stub in front of the compressed kernel; enters long mode |
arch/x86/boot/compressed/misc.c | extract_kernel(), the decompressor |
arch/x86/kernel/head_64.S | startup_64: first instructions of the real vmlinux |
arch/x86/kernel/head64.c | x86_64_start_kernel() → start_kernel() |
drivers/firmware/efi/libstub/ | the EFI stub that lets a bzImage run as a UEFI executable |
init/main.c | start_kernel(), rest_init(), kernel_init() |
init/initramfs.c | the cpio extractor that populates rootfs |
init/do_mounts.c | prepare_namespace(): mounting a real root device |
usr/gen_init_cpio.c | builds initramfs archives at kernel build time |
Documentation/arch/arm64/booting.rst | ARM64 entry contract (x0 = DTB address) |
The PID 1 hand-off in init/main.c (Linux 6.12) is short enough to read whole. rest_init() creates the two primordial tasks:
pid = user_mode_thread(kernel_init, NULL, CLONE_FS); /* → PID 1 */
...
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES); /* → PID 2 */
The initramfs /init convention is literally a default value plus an existence check:
static char *ramdisk_execute_command = "/init"; /* rdinit= overrides */
...
/* kernel_init_freeable(): */
if (init_eaccess(ramdisk_execute_command) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace(); /* no early init → mount root= device */
}
The fall-through ladder in kernel_init() is where every failed boot you will ever debug ends up dying:
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n", ramdisk_execute_command, ret);
}
if (execute_command) { /* init= from the command line */
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).", execute_command, ret);
}
...
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
Note the asymmetry: a failing initramfs /init logs and falls through, but a failing explicit init= panics immediately. You asked for it by name, so the kernel assumes fallback would be worse than stopping.
Coconut tie-in [working]§
Everything here is spec-phase design (04-HLD, 05-LLD), not shipped software.
One ISO, two install profiles. Coconut plans a single install image whose installer offers two profiles. The boot chain (ESP layout, shim, bootloader, kernel, initramfs) is identical for both, with profile differences confined to package selection and configuration applied after switch_root. One image to sign, mirror, and reproduce.
Secure Boot for a brand-new distro. Coconut inherits the political reality unfolded above: ship a shim, take it through public shim-review, get it signed by Microsoft's third-party UEFI CA. For a distro arriving in the 2026+ window, that means targeting the 2023-series CA from day one. The shim embeds a Coconut vendor certificate; Coconut's key signs the bootloader and the 6.12-fork kernel; SBAT metadata ships from the first release. Until a Microsoft-signed shim lands, early adopters enroll the Coconut certificate as a MOK, the same path planned for locally built modules (the NVIDIA/DKMS case matters because of the CUDA stack).
coconutd as PID 1. The final exec of the boot chain is where Coconut visibly diverges: instead of systemd, the initramfs hands the real root to coconutd, the planned s6-derived, greenfield-in-Rust init. It owns exactly the PID 1 duties listed above: mounts, service supervision, reaping. Agent supervision through the agent_* syscall family sits beside them as a first-class sibling. The boot path is also CI surface: the kunit-coconut gate QEMU-boots the fork's kernel on every push, the same loop as this chapter's lab.
Lab: build an initramfs from nothing, boot it, then break it [working]§
Run this one. It is about fifteen minutes, and at the end you will have watched a machine you assembled go from a compressed image to a shell prompt, then fail two different ways on purpose. Reading about the fall-through ladder and watching it execute rung by rung are not the same experience.
You need: the bzImage from Chapter 0, QEMU (brew install qemu on the macOS host), and Docker (the archive is assembled on Linux because it contains device nodes and symlinks). The Chapter 0 kernel config already includes initramfs support (CONFIG_BLK_DEV_INITRD) and the ttyS0 serial console.
1. Build the archive [working]§
mkdir -p ~/coconut-lab/ch06 && cd ~/coconut-lab/ch06
docker run -i --rm -v "$PWD":/out debian:bookworm bash -s <<'SCRIPT'
set -eu
apt-get update -qq >/dev/null
apt-get install -y -qq busybox-static cpio >/dev/null
mkdir -p /ir/bin /ir/dev /ir/proc /ir/sys
cp /bin/busybox /ir/bin/busybox
for a in sh mount ls cat ps uname dmesg sleep; do ln -s busybox "/ir/bin/$a"; done
mknod -m 600 /ir/dev/console c 5 1
cat > /ir/init <<'EOF'
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
echo
echo "=== hello from the initramfs: /init is PID $$ ==="
echo
exec /bin/sh
EOF
chmod +x /ir/init
cd /ir && find . | cpio -o -H newc | gzip -9 > /out/initramfs.cpio.gz
ls -lh /out/initramfs.cpio.gz
SCRIPT
Every piece is something this chapter named: a statically linked busybox (no shared libraries exist in this world), a /dev/console node (character device 5:1, which the kernel opens for PID 1's stdio), an executable /init (the exact default path kernel_init checks), packed as newc-format cpio and gzipped. Expect roughly a 1 MiB archive.
2. Green: boot it [working]§
qemu-system-x86_64 \
-kernel ~/coconut-lab/kernel/arch/x86/boot/bzImage \
-initrd ~/coconut-lab/ch06/initramfs.cpio.gz \
-append "console=ttyS0" \
-nographic -m 512
(Adjust the bzImage path to your Chapter 0 build. Exit QEMU with Ctrl-a then x. One honest footnote: -kernel makes QEMU itself act as the bootloader, implementing the Linux boot protocol directly. This lab exercises everything from decompression onward but skips firmware, Secure Boot, and GRUB.)
Expected: a couple of seconds of kernel log, ending approximately:
[ 0.6...] Trying to unpack rootfs image as initramfs...
...
[ 1.0...] Run /init as init process
=== hello from the initramfs: /init is PID 1 ===
/bin/sh: can't access tty; job control turned off
/ #
That is the whole chapter in four lines: the cpio unpacked into rootfs, kernel_init found /init, ran it as PID 1, and it exec'd into a shell. (The tty warning is normal. This shell has no controlling terminal, which is Chapter 8 territory.) Verify your prediction, then read early printk:
/ # cat /proc/1/comm
sh
/ # dmesg | head -25
In the dmesg output, find in order: the Linux version 6.12... banner, the Command line: console=ttyS0 echo, the physical memory map, Trying to unpack rootfs image as initramfs..., and Run /init as init process. That is printk's record of exactly the sequence this chapter unfolded.
You can now read the first second of a machine's life in the machine's own words, and say which stage produced each line. That is the skill that turns a wall of boot log into a timeline with a failure point in it.
3. Red: break the init path [working]§
qemu-system-x86_64 \
-kernel ~/coconut-lab/kernel/arch/x86/boot/bzImage \
-initrd ~/coconut-lab/ch06/initramfs.cpio.gz \
-append "console=ttyS0 rdinit=/bin/nope" \
-nographic -m 512
Expected, and this one surprises almost everyone:
[ 1.0...] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
A mount panic for an exec mistake. Re-read the kernel_init_freeable snippet and it makes sense: /bin/nope failed the existence check, so the kernel concluded "there is no early userspace here" and fell back to prepare_namespace(), which mounts a real root device. We passed no root= and attached no disk, so that is what dies. Lesson for real boot failures: this famous panic means "the kernel never got a usable root or early init," and a missing or misnamed initramfs /init is one of its causes.
4. Red, variant: a present-but-broken init [working]§
Rebuild the archive with one change: make /init's first line #!/bin/nosuchshell. Now the file exists (the existence check passes) but exec fails because its interpreter is missing. Predict: panic, or something else? Expected, approximately:
[ 1.0...] Failed to execute /init (error -2)
[ 1.0...] Run /sbin/init as init process
[ 1.0...] Run /etc/init as init process
[ 1.0...] Run /bin/init as init process
[ 1.0...] Run /bin/sh as init process
/bin/sh: can't access tty; job control turned off
/ #
No panic. You watched kernel_init()'s fall-through ladder execute live, rung by rung, until /bin/sh (which busybox provides) caught it. Error -2 is -ENOENT: the missing interpreter. Nothing mounted /proc this time, so the rescue shell is bare. Finally, from either working shell, type exit: PID 1 dies, and the kernel panics with a message about attempting to kill init. That is the "PID 1 must not die" rule, demonstrated.
Three boots, three outcomes, and you can name the line of init/main.c responsible for each. Next time a machine hangs on you, the question stops being "why won't it boot" and becomes "which stage printed last, and what does the stage after it need that it did not get." Try one more variant while the archive is still warm: drop the mknod line for /dev/console, rebuild, and boot. PID 1 starts with no stdio, so the shell runs and prints nothing. That failure mode looks identical to a hang, and now you know it is not one.
Bridge notes [working]§
Reset vectors: you know this one. On a Cortex-M or AVR you wrote the vector table yourself and execution began at a fixed flash address. x86 is the same idea at 0xFFFFFFF0; the delta is that a PC's "startup code" is a multi-megabyte firmware you did not write, whose job (DRAM training, PCIe enumeration, boot policy) exists because PC hardware is variable and discoverable while your MCU's memory map was fixed in the datasheet.
Flashing vs bootstrapping. On an MCU you flashed one final image over JTAG; no boot problem, because no loading. A PC must find and verify its OS on arbitrary storage, hence the staged chain. Think of shim and GRUB as progressively richer crt0s, each establishing the runtime the next stage assumes.
The setup header is an ABI. From compilers you know calling conventions: fields at agreed offsets, caller fills, callee reads. The Linux boot protocol is exactly that, between bootloader and kernel: header.S is the struct, boot.rst the spec. The bzImage itself is a self-extracting artifact, a linked stub whose payload is another fully linked ELF.
Device tree = your board support, as data. The .dts you may have met in embedded work formalizes the memory map and peripheral list you hand-maintained for a microcontroller. Same information, parsed by a generic kernel instead of hardcoded.
Sources [fundamental]§
- https://mjg59.dreamwidth.org/66109.html: reset vector at 0xFFFFFFF0, real-mode entry with preset CS base
- https://wiki.osdev.org/Boot_Sequence: BIOS loads 512-byte sector to 0x7C00, 0x55AA signature
- https://uefi.org/specs/UEFI/2.10/03_Boot_Manager.html: Boot#### / BootOrder NVRAM variables, boot manager behavior
- https://manpages.ubuntu.com/manpages/focal/man8/efibootmgr.8.html: editing UEFI boot entries from Linux
- https://www.systemshardening.com/articles/linux/linux-uefi-secure-boot-db/: PK/KEK/db/dbx hierarchy, shim and MOK flow
- https://kb.cert.org/vuls/id/616257: shim signed by Microsoft's third-party UEFI CA; trust-chain mechanics
- https://techcommunity.microsoft.com/blog/windows-itpro-blog/act-now-secure-boot-certificates-expire-in-june-2026/4426856: 2011 CA expiry June 2026, 2023-series replacements
- https://www.welivesecurity.com/en/eset-research/forgotten-uefi-shims-undermining-secure-boot/: expiry does not un-trust signed binaries; db/dbx govern
- https://manpages.ubuntu.com/manpages/xenial/man1/mokutil.1.html: MOK enrollment via mokutil/shim
- https://github.com/rhboot/shim/blob/main/SBAT.md: SBAT generation-number revocation
- https://www.kernel.org/doc/html/v6.12/filesystems/ramfs-rootfs-initramfs.html: initramfs = gzipped cpio into rootfs; /init; switch_root vs pivot_root (pinned to v6.12, because the unversioned mainline page has since been rewritten and now describes the post-6.12 nullfs-root behavior)
- https://docs.kernel.org/admin-guide/efi-stub.html: bzImage masquerading as PE/COFF; CONFIG_EFI_STUB
- https://wiki.archlinux.org/title/Systemd-boot: systemd-boot launches EFI executables only
- https://docs.kernel.org/arch/arm64/booting.html: ARM64 entry contract: DTB physical address in x0; DT/ACPI requirements
- https://asahilinux.org/docs/platform/introduction/: Apple Silicon SecureROM → iBoot chain
- https://asahilinux.org/docs/alt/boot-process-guide/: m1n1 → U-Boot(+DT) → standard bootloader → Linux
- https://elixir.bootlin.com/linux/v6.12.6/source/init/main.c: start_kernel/rest_init/kernel_init; init fallback ladder and panic strings verified against the 6.12 tree
- https://0xax.gitbooks.io/linux-insides/Booting/linux-bootstrap-5.html: extract_kernel() decompression path
- https://github.com/torvalds/linux/blob/master/arch/x86/boot/compressed/head_64.S: compressed-kernel entry stub
- https://mudongliang.github.io/2017/09/12/how-to-build-a-custom-linux-kernel-for-qemu.html: busybox initramfs + QEMU -kernel/-initrd invocation pattern