Chapter 0: Think Like an OS§
After this chapter you can state precisely what an operating system is: a multiplexer, an abstraction machine, and a protection referee. You can defend each word. You can tell a kernel from an OS from a distribution, place Linux, seL4, and XNU on the design map, and explain why building Coconut OS forces every topic in this book.
Two kinds of reader want that for different reasons and get the same answer. If you are heading for kernel work, those three jobs are the job description. If you are heading for a latency-sensitive desk, they are what decides when your code runs, when it gets taken off the CPU without being asked, and how many privilege boundaries a packet crosses before it reaches your hot loop. Both of you are asking the same thing: what the machine is doing when it is not running your code.
And your lab bench will exist: QEMU installed on your Mac, the Linux 6.12 LTS source tree in a Docker cross-build container, and a kernel you configured and compiled yourself, booting to an init program you wrote. The whole loop, end to end, before Chapter 1.
The problem [fundamental]§
Take away the operating system and see what is left. A modern laptop is a handful of CPU cores, some RAM, an SSD, a network interface, and a screen. Nothing about that hardware knows what a "program" is, let alone two hundred of them. Point the CPU at some machine code and it will execute instructions, one after another, forever. That is the entire service the silicon offers.
Now try to run a browser and a music player at the same time on that bare machine. Three things break immediately.
First, sharing. One CPU core runs one instruction stream. If the browser has the core, the music player is dead. Not paused, dead: nothing exists to take the core away from the browser and hand it over. Cooperative schemes ("every program promises to yield regularly") fail the moment one program has a bug, an infinite loop, or a malicious author. One misbehaving program starves the machine.
Second, hardware knowledge. On the bare machine, any program that wants to write a file must contain a driver for your exact SSD, code to lay out bytes in a filesystem format, and logic to avoid trampling other programs' data. Every application would reimplement all of it, incompatibly. The 1950s actually worked this way (programmers wrote their own I/O routines per machine), and it was miserable enough that shared "monitor" programs, the ancestors of operating systems, appeared within a decade.
Third, protection. On the bare machine, memory is one flat expanse of bytes. The music player can read the browser's memory, your banking session and your cookies included, by loading from the right address. It can also write there, by accident. A single wild pointer in any program can corrupt every other program. There is no "crash"; there is only shared, silent ruin.
An operating system is the program we install to solve exactly these three failures. Everything else (files, processes, users, windows, containers) is machinery in service of those three jobs.
Unfolded [fundamental]§
Job one: the multiplexer [fundamental]§
To multiplex is to make one physical resource behave like many private ones. The OS multiplexes every scarce resource in the machine.
For the CPU, it uses time. The kernel is the OS's privileged core, defined properly in a moment. It lets a program run for a few milliseconds, then a hardware timer fires, control returns to the kernel, and the kernel picks the next program to run. This is a context switch: the kernel saves every CPU register the current program was using, restores the saved registers of another program, and jumps back into it. Done tens or hundreds of times per second per core, it produces the illusion that every program owns a CPU.
Notice which way the force points. The timer takes the CPU away from a program that never volunteered to stop, at a moment the program does not choose. If you are writing code where a delayed microsecond costs money, that involuntary pause is the thing you will spend years learning to bound. The policy for choosing who runs next is called scheduling, and it gets a full chapter (Chapter 9), because "pick fairly" turns out to be a deep and contested problem.
For memory, it uses translation. Each program sees its own private range of addresses starting at zero; hardware inside the CPU (the MMU, Chapter 3) translates each program's pretend addresses to different physical RAM. Two programs can both use "address 0x400000" and never touch each other.
For devices, it uses queues. Only the kernel talks to the SSD or the network card; programs submit requests, the kernel orders and batches them, and results come back asynchronously.
The multiplexing is the illusion of privacy. The next job is the illusion of meaning.
Job two: the abstraction machine [fundamental]§
Here is the claim to internalize, because this book returns to it constantly: processes, files, and sockets do not exist in hardware. They are useful fictions that the kernel invents and then maintains with bookkeeping.
A process is the fiction of a running program that owns a machine: its own CPU (really: time slices), its own memory (really: translated pages), its own identity. In reality, a process is a set of data structures in kernel memory recording which registers to restore, which memory translations apply, which files are open, and who is allowed to do what. When people say "the kernel killed the process," they mean the kernel freed those structures and stopped scheduling that register state. Nothing physical died.
A file is the fiction of a named, byte-addressable, durable sequence of bytes. Your SSD offers nothing of the sort. It offers fixed-size blocks at numeric addresses, with awkward rules about erasing. The filesystem (Chapters 14 and 15) is the kernel code that maintains the mapping from "essay.txt, byte 4096" to "block 88213, offset 0," plus the directory tree, plus safety machinery so a power cut mid-write does not shred the fiction.
A socket is the fiction of a reliable two-way pipe to another machine. The network offers unreliable packets that arrive late, duplicated, or never. The kernel's network stack (Chapter 23) manufactures reliability out of unreliability with sequence numbers, acknowledgments, and retransmission.
Why fictions? Because they are the interface. A program written against "open a file, read bytes" runs unchanged whether the bytes live on an SSD, a spinning disk, or a network share. The abstraction machine is what makes software portable across hardware, and it is why the kernel's abstractions, not its implementation, are its real product. Break the implementation and you patch it; break the abstraction's contract and you break every program ever written against it. This is why kernel developers treat userspace-visible behavior as near-sacred.
Job three: the protection referee [fundamental]§
Fictions need enforcement. If any program could reprogram the memory-translation hardware, "private memory" would be a suggestion. So CPUs provide privilege levels: a mode bit deep in the processor that says whether the currently executing code is allowed to run the dangerous instructions: the ones that change memory translations, talk to devices, or disable interrupts. Kernel code runs in the privileged mode ("kernel mode"); everything else runs deprivileged ("user mode"). Chapter 1 covers what this means physically on x86_64 and ARM64.
A user-mode program that needs a privileged service (read a file, send a packet, start a process) cannot call kernel code like a normal function. It executes a special instruction that atomically switches the CPU to kernel mode and jumps to a single kernel-chosen entry point. This controlled doorway is the system call, or syscall (Chapter 7). The kernel checks the request against policy. Whether this process may read this file is decided here, not in the program that asked. Then the kernel performs the request or refuses, and returns to user mode. Every one of your program's interactions with the world funnels through this door, which is what makes the referee job possible at all: there is exactly one place to stand and check.
That one door is also a toll booth, which is why latency-sensitive systems count their crossings. Every trip through it is a hardware privilege change the program pays for, and the cheapest work is the work that never crosses. Chapter 7 counts the crossings a hello-world actually makes, and shows the fast path the kernel built to avoid some of them.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ browser │ │ player │ │ shell │ user mode
└────┬─────┘ └────┬─────┘ └────┬─────┘ (deprivileged)
│ syscall │ syscall │ syscall
═════╪═════════════╪═════════════╪═════════ the one door
┌────┴─────────────┴─────────────┴─────┐
│ k e r n e l │ kernel mode
│ scheduler · memory · VFS · net · … │ (privileged)
└────┬───────────┬───────────┬─────────┘
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ CPUs │ │ RAM │ │ devices │ hardware
└─────────┘ └─────────┘ └─────────┘
Who is allowed to do what (identity, permissions, capabilities) is the entire subject of Part V, and it is where Coconut OS diverges hardest from the Unix inheritance.
Kernel, OS, distribution: three different things [fundamental]§
These terms get blurred in casual speech; this book keeps them separate.
| Term | What it is | Example |
|---|---|---|
| Kernel | The one program that runs in privileged CPU mode and implements the three jobs | Linux, seL4, XNU |
| Operating system | Kernel plus the essential userspace it needs to be usable: init system, C library, core utilities, service managers | GNU/Linux, macOS (XNU + Darwin userspace) |
| Distribution | A curated, versioned, installable product: an OS plus a package manager, an installer, update channels, default configuration, and a support lifecycle | Debian, Fedora, Coconut OS |
"Linux" strictly names only a kernel. Debian and Fedora ship that same kernel wrapped in different userspace choices and packaging. Coconut OS is a distribution, but an unusual one: it also forks the kernel itself rather than shipping stock Linux. That dual nature is why this book must cover both kernel internals (Parts I to VI) and the userspace/distribution contract (Parts VII to IX).
The shape of a kernel: monolithic, micro, hybrid [working]§
Given that some code must run privileged, the central design question is: how much? Every real kernel is an answer to that question.
A monolithic kernel answers "most of it." Device drivers, filesystems, the network stack: all run in kernel mode, in one shared address space, calling each other as ordinary functions. Linux is the canonical example, and the source tree passed roughly 40 million lines around the 6.12 era (drivers dominate that count). The win is performance and simplicity of interaction. A filesystem calling a disk driver is a function call, nanoseconds. The cost is blast radius: a bug in any driver is a bug in the kernel, with full privileges. Linux softens the edges with loadable modules and in-kernel extension points: LSM security hooks (Chapter 19), eBPF (Chapter 35), and the sched_ext pluggable-scheduler framework that merged in 6.12 itself. Architecturally it is still monolithic, and its maintainers are unapologetic about it.
A microkernel answers "almost none of it." The kernel provides only the irreducible privileged minimum: address spaces, threads, and inter-process communication (IPC). Everything else, including drivers and filesystems, runs as ordinary user-mode processes that message each other. seL4 is the modern flagship: a high-assurance microkernel on the order of ten thousand lines of C (the original verified kernel was about 8,700 lines of C), small enough that its implementation has been formally proved to match its specification, machine-checked in the Isabelle/HOL theorem prover. The win is isolation and assurance: a crashed driver is a crashed process, restartable, and the privileged core is small enough to prove correct. The cost is that every interaction that was a function call becomes an IPC round trip through the kernel, and decades of microkernel engineering have been spent making that fast enough.
A hybrid kernel splits the difference: microkernel-style internal structure, but the services linked into one privileged address space anyway, keeping function-call performance. Apple's XNU, the kernel under macOS and iOS, is the textbook case. Apple's own source README describes it as "a hybrid kernel combining the Mach kernel developed at Carnegie Mellon University with components from FreeBSD and a C++ API for writing drivers called IOKit." Mach's message-passing abstractions are real and used, but the BSD layer and drivers do not pay a user-mode boundary crossing to reach them.
MONOLITHIC (Linux) MICROKERNEL (seL4) HYBRID (XNU)
┌───────────────────┐ ┌────┐ ┌────┐ ┌───────┐ ┌───────────────────┐
│ apps │ │apps│ │ FS │ │drivers│ │ apps │
╞═══════════════════╡ ╞════╧═╧════╧═╧═══════╡ ╞═══════════════════╡
│ drivers · FS · net│ │ IPC · threads · │ │ Mach + BSD + IOKit│
│ sched · mm · IPC │ │ address spaces │ │ in one address │
│ (one address │ │ (~10 kLOC, proved) │ │ space │
│ space, ~40 MLOC)│ └─────────────────────┘ └───────────────────┘
└───────────────────┘ services live in user micro structure,
everything privileged mode; kernel is minimal monolithic privilege
| Linux (monolithic) | seL4 (microkernel) | XNU (hybrid) | |
|---|---|---|---|
| Privileged code size | ~40M-line tree (built subset varies) | ~10 kLOC | Millions of lines |
| Drivers live | In kernel | User mode | In kernel (IOKit) |
| Driver bug blast radius | Whole system | One process | Whole system |
| Service-to-service call | Function call | IPC message | Function call |
| Assurance story | Review + testing + tooling | Machine-checked proof | Review + testing |
| Extension story | Modules, eBPF, LSM, sched_ext | User-mode servers, capabilities | Kexts (deprecated), user-mode DriverKit |
Why does this book build on monolithic Linux while Chapter 20 spends serious time on seL4? Because Coconut OS takes Linux's ecosystem (the drivers, the software compatibility) and imports the microkernel world's best idea, capability-based authority, into it. You need to understand both traditions to see why that is hard and worth doing.
If the three-way comparison did not fully land, carry one sentence forward and lose nothing: the design question is how much code runs privileged, and every kernel is an answer to it. Chapter 20 comes back and does this properly, with the capability model in hand.
The Coconut north star [working]§
Coconut OS is a hard fork of Linux 6.12 LTS built around one thesis: AI agents should be first-class kernel primitives, the way processes, files, and users are today. It is currently in spec phase, pre-implementation; the designs live in 04-HLD and 05-LLD. Not "an agent is a process with a config file," but: the kernel knows what an agent is, schedules it under agent-aware policy, grants it authority through explicit capability tokens instead of inherited Unix permissions, and records everything it does in a tamper-evident audit stream.
Chapter 37 makes the full argument. What matters in Chapter 0 is that building this system touches every subsystem an OS has. That is why it works as the spine of a fundamentals book. The map:
| Coconut piece | What it is (spec-phase design) | Fundamentals chapter |
|---|---|---|
| agent_* syscall family (numbers 472 to 479; 472 agent_spawn and 473 agent_attest wired, 474 to 479 reserved -ENOSYS stubs) | New kernel entry points for agent lifecycle | Ch 7 (syscall boundary), Ch 8 (processes) |
| SCHED_AGENT scheduling class (planned; tenant-fairness lineage from kvwarden, Coconut Labs' CUDA inference broker) | Agent-aware CPU scheduling | Ch 9 (scheduling) |
| Capability system: v1 additive cred shim + LSM hooks + mm tier-aware hooks; v2 full replacement of Unix DAC | Explicit-authority security model | Ch 17 (creds/DAC), Ch 19 (LSM), Ch 20 (capabilities) |
| kernel/audit/coconut: JSON-lines, optional zstd, BLAKE3 hash chain | Tamper-evident audit | Ch 21 (audit) |
| coconutd: PID 1, s6-derived greenfield init in Rust | Service management | Ch 26 (init), Ch 34 (Rust) |
| coconutpkg: rpm-ostree fork, A/B atomic updates with rollback | Distribution mechanics | Ch 27 (packaging) |
| Coconut Shell: Wayland compositor in Rust on Smithay | The interaction surface | Ch 29 (Wayland) |
Hardware targets are staged: v1.0 is x86_64 only, v1.1 adds ARM64 server, and v1.2 lists Apple Silicon as a stretch goal conditional on Asahi Linux's velocity. That is why every lab in this book targets x86_64, including the one you are about to run on an ARM Mac. That is exactly what a cross-compiler is for.
The skills you are signing up for [working]§
Four competencies recur through the book. None needs mastery today; Chapter 0's job is to name them so nothing later is a surprise.
Kernel C. The kernel is written in C, specifically GNU C. Since Linux 5.18 the tree builds as -std=gnu11 (C11 plus GNU extensions), after decades on gnu89. It is also freestanding C: there is no libc in the kernel. No printf (the kernel has printk), no malloc (it has kmalloc and friends), no standard headers. The kernel carries its own implementations of everything, because libc itself is built on syscalls, and the kernel is the thing underneath syscalls. Expect heavy use of GNU extensions your compiler course skipped: typeof, statement expressions, attribute annotations, designated initializers everywhere.
Rust. Coconut writes new userspace components (coconutd, Coconut Shell) in Rust, and Part X covers Rust-for-Linux in the kernel proper. You need reading-level Rust by Part VII and working Rust by Part X. If you have it, fine; if not, the official book plus the labs will get you there incrementally.
Git, fluently. Past commit and push: git log --oneline -- <path> to read a subsystem's history, git blame to find why a line exists, git bisect to find which commit broke a boot. Kernel development is git archaeology much of the time, and Chapter 33 covers maintaining a fork professionally.
Reading kernel source. Nobody reads 40 million lines linearly. The working method has two moves. Use grep -rn (or ripgrep) for symbols inside the tree. Use elixir.bootlin.com, a web cross-referencer over every kernel version, to jump from a symbol to its definition and every caller. When this book names a struct, the expected move is: open elixir, pin the version selector to v6.12, and look at it.
The real thing in Linux [working]§
Everything above has a street address in the 6.12 tree. Untar the source (the Lab does this) and the top level is the map of the whole book:
linux-6.12.103/
├── arch/ per-CPU-architecture code; arch/x86/ is our world until v1.1
├── kernel/ the core: scheduler, fork, time, locking → Parts II
│ └── sched/ scheduling classes; core.c holds __schedule()
├── mm/ memory management → Part III
├── fs/ VFS and filesystems → Part IV
├── security/ LSM framework, SELinux, AppArmor → Part V
├── net/ the network stack → Part VI
├── drivers/ the majority of those ~40M lines
├── include/ headers; include/uapi/ is the userspace ABI contract
├── init/ boot-time init; main.c holds start_kernel()
└── tools/ userspace tools that ship in-tree, incl. testing
The fictions from "Unfolded" are concrete structs:
- The process is
struct task_struct, defined ininclude/linux/sched.h. It carries hundreds of fields: saved scheduling state, a pointer to the memory descriptor (mm), the credentials (cred), the open-file table, the process ID. Every process, and every thread, is one of these in kernel memory. - The scheduler's core loop is
__schedule()inkernel/sched/core.c, the function that actually picks the nexttask_structand context-switches to it. Chapter 9 walks it. - The open file is
struct fileininclude/linux/fs.h, whosef_opfield points to astruct file_operations, a table of function pointers (read,write,mmap, …) supplied by whatever filesystem or driver backs the file. This function-pointer dispatch is how "everything is a file" is implemented: same interface, swappable implementation. Your compiler course called this a vtable. - The syscall table for 64-bit x86 is literally a text file:
arch/x86/entry/syscalls/syscall_64.tbl, mapping syscall numbers to handler functions. Aread()from userspace lands inksys_read()infs/read_write.c, which resolves the file descriptor to astruct fileand dispatches throughf_op. Chapter 7 traces the full path instruction by instruction. - The referee's hook points live in
security/security.c, where the Linux Security Module (LSM) framework interposes on security-relevant operations. That is the mechanism Coconut's capability enforcement attaches to.
Version context, pinned to this writing (August 2026): 6.12 was released on 17 November 2024 and is a longterm-support (LTS) series, currently at 6.12.103. Its projected end of life was extended in early 2026 to December 2028 (from an original December 2026), driven by its adoption base. It is the first mainline kernel with PREEMPT_RT real-time support built in, and it also merged sched_ext. Mainline has since moved on (7.x, with 6.18 the newest LTS). A fork must pick a base and live with it, which is Chapter 33's subject.
PREEMPT_RT is the line to notice if you came here from the determinism side of the industry. It is the configuration that makes the kernel's own critical sections preemptible, so a high-priority thread waits on a bounded delay instead of on whatever the kernel happened to be in the middle of. Coconut's base has it available because 6.12 is the release where it landed.
Coconut tie-in [working]§
Coconut OS forks exactly this 6.12 LTS tree, chosen for the December 2028 support runway, with a planned post-GA rebase onto 6.18 LTS. The fork adds three new subsystem directories you will not find upstream: kernel/agent/ (agent lifecycle), security/coconut/ (capability enforcement, attached via the LSM hook points above), and kernel/audit/coconut/ (the BLAKE3-chained JSON-lines audit stream). It extends arch/x86/entry/syscalls/syscall_64.tbl with the agent_* family at 472 to 479. Two are wired (472 agent_spawn, 473 agent_attest) and six are reserved as -ENOSYS stubs, so the ABI surface is claimed before it is implemented. All of this is spec-phase design per 04-HLD and 05-LLD, not shipped code; where this book shows Coconut design, it says so.
One piece of Coconut practice arrives in this very chapter. The project's CI runs a kunit-coconut gate on every push: build the kernel, boot it under QEMU x86_64, run the unit tests. The lab bench you are about to build is the same architecture in miniature: cross-compile in a Linux container, boot in QEMU, observe. Learning environment and production gate, one workflow.
Lab [fundamental]§
Goal: from a stock Mac to a self-built Linux 6.12 kernel booting your own PID 1 in QEMU. Everything is copy-paste; predictions come before each run. You need Homebrew and Docker Desktop (or colima) already installed. Commands were authored against the versions cited in Sources; expected outputs show the shape, not byte-exact text.
Nothing here needs a spare machine, a partition, or a reinstall. Every risky step happens inside a container or an emulator, and the worst outcome in the emulator is a stuck QEMU you kill with Ctrl-a x. Run the commands rather than reading them. What this lab leaves you with is a kernel you have actually booted, and reading about booting one does not produce that.
Lab 0.1: QEMU on the Mac [fundamental]§
QEMU is an open-source machine emulator: it implements a whole PC (CPU, RAM, serial port) as a userspace program, letting you boot experimental kernels with zero risk to your host.
brew install qemu
qemu-system-x86_64 --version
Expected:
QEMU emulator version 11.1.0
(11.1.0 is the Homebrew formula version at this writing; newer is fine.) The binary name matters: qemu-system-x86_64 emulates a full 64-bit PC. On an ARM Mac it emulates x86_64 in software (TCG). That is slower than native and entirely sufficient for a tiny kernel.
Lab 0.2: the cross-build container [fundamental]§
Kernels are built on Linux. Rather than dual-boot, we build inside a Docker container. One rule up front: keep the kernel tree inside the container's filesystem, not in a bind-mounted Mac folder. macOS's default filesystem is case-insensitive, and the kernel tree contains header pairs differing only by case (netfilter's xt_dscp.h vs xt_DSCP.h) that collide on it. Bind mounts are slow for a tree this size on top of that.
docker run -it --name kbench ubuntu:24.04 bash
Inside the container:
apt update && apt install -y build-essential gcc-x86-64-linux-gnu \
flex bison bc libssl-dev libelf-dev cpio xz-utils curl
x86_64-linux-gnu-gcc --version | head -1
Expected:
x86_64-linux-gnu-gcc (Ubuntu 13.x...) 13.x.x
On an ARM Mac this container is ARM64, so gcc-x86-64-linux-gnu is a cross-compiler: it runs on ARM64 and emits x86_64 code, which is Coconut's v1.0 target. Compilation runs at native speed, with no emulation. (On an Intel Mac the container is already x86_64; the same commands work, or drop the cross package and CROSS_COMPILE and use plain gcc.)
Lab 0.3: the source [fundamental]§
Kernel releases are published at kernel.org; tarballs follow the pattern https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-<version>.tar.xz. We pin the exact point release current at this writing:
mkdir -p /src && cd /src
curl -LO https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.103.tar.xz
tar xf linux-6.12.103.tar.xz && cd linux-6.12.103
ls
Expected: the top-level map from "The real thing in Linux", namely arch/ drivers/ fs/ kernel/ mm/ net/ security/ .... Check kernel.org for the newest 6.12.y and substitute; any 6.12.y works identically here.
Lab 0.4, red: the smallest kernel, and why it says nothing [fundamental]§
The kernel's build system generates a configuration (thousands of on/off options) and compiles the selected subset. tinyconfig selects the bare minimum that compiles.
make ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- tinyconfig
make ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- -j"$(nproc)" bzImage
ls -lh arch/x86/boot/bzImage
Expected: a few minutes of compilation, ending near
Kernel: arch/x86/boot/bzImage is ready (#1)
with a bzImage (the compressed bootable kernel) around half a megabyte, out of the same source tree that builds 100+ MB distro kernels. Copy it to the Mac and boot it (in a Mac terminal):
docker cp kbench:/src/linux-6.12.103/arch/x86/boot/bzImage /tmp/bzImage-tiny
qemu-system-x86_64 -m 512M -nographic -kernel /tmp/bzImage-tiny
Expected:
SeaBIOS (version ...)
Booting from ROM..
...and then nothing, forever. This is the red. The kernel is running. But tinyconfig disables CONFIG_PRINTK (kernel logging) and every console driver, so it is mute, and it has no program to run, so even a healthy boot ends in a panic you cannot see. A kernel with no output and no userspace is indistinguishable from a hang. That is a lesson the Coconut CI history taught the hard way. Exit QEMU with Ctrl-a x.
If you sat looking at that blank screen wondering what you had mistyped, that reaction is the point of the lab, and it does not go away with experience. It is why kernel engineers keep a serial console attached to everything: from the outside, a machine with nothing to say and a machine that has died look exactly the same. The next lab is the fix, and it is seven config options long.
Lab 0.5, green: a kernel that talks, and a PID 1 you wrote [fundamental]§
Back in the container, enable exactly what the silence lacked: logging, a serial console, initramfs loading, and ELF execution.
cd /src/linux-6.12.103
./scripts/config --enable CONFIG_PRINTK --enable CONFIG_TTY \
--enable CONFIG_SERIAL_8250 --enable CONFIG_SERIAL_8250_CONSOLE \
--enable CONFIG_BLK_DEV_INITRD --enable CONFIG_RD_GZIP \
--enable CONFIG_BINFMT_ELF
make ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- olddefconfig
make ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- -j"$(nproc)" bzImage
Now userspace. When the kernel finishes booting it executes exactly one program, historically /sbin/init, PID 1. Everything else on a Linux system descends from it. Ours will be honest about what PID 1 minimally is:
cat > /src/init.c <<'EOF'
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("hello from PID %d - this is init, and I wrote it\n",
(int)getpid());
fflush(stdout);
for (;;)
pause(); /* PID 1 must never exit */
return 0;
}
EOF
Package it as an initramfs, a compressed cpio archive (an old Unix format, like tar) that the kernel unpacks into a RAM-backed root filesystem at boot. One subtlety: the kernel wires PID 1's stdin/stdout to the device node /dev/console, and an initramfs starts empty, so we must create that node ourselves (c 5 1 is the console device's type and major/minor number; Chapter 5 explains device numbering):
mkdir -p /src/initramfs/dev
x86_64-linux-gnu-gcc -static -o /src/initramfs/init /src/init.c
mknod -m 600 /src/initramfs/dev/console c 5 1
cd /src/initramfs
find . | cpio -o -H newc | gzip > /src/init.cpio.gz
-static matters: the initramfs contains no C library for dynamic linking, so the binary must carry everything. Predict: what will boot print now, and what will be the number in "PID %d"? Then, on the Mac:
docker cp kbench:/src/linux-6.12.103/arch/x86/boot/bzImage /tmp/bzImage
docker cp kbench:/src/init.cpio.gz /tmp/init.cpio.gz
qemu-system-x86_64 -m 512M -nographic \
-kernel /tmp/bzImage -initrd /tmp/init.cpio.gz \
-append "console=ttyS0 rdinit=/init"
-append passes the kernel its command line: log to the first serial port (which -nographic connects to your terminal), and run /init from the initramfs as PID 1. Expected:
Linux version 6.12.103 (root@...) (x86_64-linux-gnu-gcc ...) ...
...
Run /init as init process
hello from PID 1 - this is init, and I wrote it
Green. You configured, cross-compiled, and booted a Linux kernel, and the first process it ran was yours.
Two closing experiments, both cheap because neither needs a kernel rebuild. First, change for (;;) pause(); to return 0;, rebuild only the initramfs, reboot, and predict what the kernel does when PID 1 exits. You should meet Kernel panic - not syncing: Attempted to kill init!, which is the kernel refusing to run a world with no userspace. Second, restore the loop and keep this bench. docker start -ai kbench resumes it, and every subsequent chapter's lab builds on it.
What you can do now that you could not before: take an arbitrary kernel source tree, decide what goes into it, produce a bootable image from it, and watch that image run code you wrote, with a console you can read the whole way down. Plenty of engineers ship against Linux for a decade without once seeing that boundary from the inside. When Chapter 6 traces boot and Chapter 7 traces a syscall instruction by instruction, you will be reading about a machine you have already started yourself.
Bridge notes [working]§
Mappings from your compiler and microcontroller coursework to this chapter's scale:
| You already know | What is new at OS scale |
|---|---|
| Bare-metal microcontroller code: your firmware owns the whole address space, talks to registers directly, one program forever | That is exactly life without an OS. Chapter 0's "problem" section is your 8051/AVR world with hostile cohabitants. The OS exists because server-class machines run many mutually distrusting programs |
| Interrupts on a microcontroller: an ISR fires, you handle it, return | Same hardware idea, but the OS uses the timer interrupt to forcibly take the CPU away from code that never volunteered to stop. Preemption is an interrupt plus a context switch (Ch 2, 9) |
| Vtables / dynamic dispatch from compiler design | struct file_operations is a hand-rolled vtable in C: same-shaped function-pointer tables giving "everything is a file" its polymorphism |
| Calling conventions and ABI from codegen | A syscall is a calling convention that additionally crosses a hardware privilege boundary. Registers carry arguments, but the "call" swaps the CPU's mode (Ch 7) |
| Freestanding vs hosted implementations (from the C standard, via compilers) | The kernel is the canonical freestanding program: no libc beneath it, because it is what libc stands on |
| Cross-compilation (you targeted micros from a PC) | Identical concept, bigger target: today you targeted x86_64 from ARM64, and the artifact was an OS kernel instead of firmware |
| Linking static binaries | The initramfs -static requirement is the loader/linking story of Ch 24 seen from the consumer side |
Sources§
- https://www.kernel.org/ : current release table, 6.12.103 as latest 6.12 longterm; 6.18 series and 7.x mainline current (checked 2026-08)
- https://endoflife.date/linux : 6.12 release date 17 Nov 2024; projected EOL 31 Dec 2028; 6.18 LTS dates
- https://www.xda-developers.com/some-linux-kernels-just-got-a-nice-lts-end-of-life-date-extension/ : early-2026 LTS extension moving 6.12 EOL from Dec 2026 to Dec 2028; adoption rationale (PREEMPT_RT, Debian 13, RHEL 10)
- https://www.phoronix.com/news/Linux-6.12-Released : 6.12 release contents (PREEMPT_RT mainlined, sched_ext merged)
- https://www.phoronix.com/news/Linux-5.18-C11-Plan : kernel C dialect move from gnu89 to gnu11 in 5.18
- https://sel4.systems/About/ : seL4 self-description (high-assurance, high-performance OS microkernel; the whole kernel is formally verified)
- https://cacm.acm.org/research/sel4-formal-verification-of-an-operating-system-kernel/ : the seL4 verification paper, ~8,700 lines of C machine-checked in Isabelle/HOL
- https://sel4.systems/About/FAQ.html : current verified-kernel sizes per configuration (~10,000 to 16,000 SLOC as of 2025)
- https://github.com/apple-oss-distributions/xnu : XNU README, hybrid kernel combining Mach, FreeBSD components, IOKit
- https://ostechnix.com/linux-kernel-source-code-surpasses-40-million-lines/ : Linux tree crossing ~40M lines in the 6.12 era
- https://formulae.brew.sh/formula/qemu :
brew install qemu; formula version 11.1.0 at this writing - https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.103.tar.xz : tarball URL pattern verified live (HTTP 200)
- https://elixir.bootlin.com/linux/v6.12/source/include/linux/sched.h : task_struct location in the v6.12 tree; elixir as the reading tool
- https://z49x2vmq.github.io/2020/12/24/linux-tiny-qemu/ : tinyconfig-to-QEMU minimal boot recipe (8250 serial console + ELF binfmt config options)