Overview & Prerequisites
This page is the setup gate. It tells you what hardware and software you need, what you should already know, how to get a kernel tree, and how to prove your rig works before you spend an evening debugging your environment instead of the kernel.
Do not start Foundations until every command on this page runs on your machine.
Platform Scope
Kernel development wants a Linux host. This is not a preference; the toolchain, the tracing tools, the module loader, and the entire mental model assume you are on one.
| Host | Status | What actually happens |
|---|---|---|
| Linux, x86-64 | Ideal | Everything in this book works natively. Build for your own architecture, boot in QEMU with KVM, run the resulting kernel on the metal if you want. |
| Linux, arm64 | Ideal | Same, targeting arm64. Every lab works; a handful of examples name x86 files, and the arm64 equivalent is always given. |
| macOS (Apple Silicon or Intel) | Needs a Linux VM | You cannot usefully build Linux on macOS: the kernel build wants GNU tools, a case-sensitive filesystem, and ELF-native binutils. Run a Linux VM and do all the work inside it. See below. |
| Windows | Use WSL2 | WSL2 is a real Linux kernel with a real userland. Building works. Nested QEMU works but is slower; check /dev/kvm exists. |
| A cloud VM | Fine, and often best | A 16-core box for an hour costs less than lunch and builds a kernel in three minutes instead of forty. Nested virtualization must be enabled for KVM inside it — otherwise QEMU falls back to emulation and boots take ~30 s instead of ~2 s. |
If you are on macOS
Do not fight this. Create a Linux VM and treat your Mac as a terminal.
# Lima is the least-friction option on Apple Silicon. UTM and Parallels also work;
# so does any cloud VM you can ssh into.
brew install lima
limactl start --name=kdev template://ubuntu-lts # accept the defaults, then edit CPUs/disk
limactl shell kdev
Inside that VM you are on Linux arm64, and every lab works. Give it at least 4 CPUs, 8 GB of RAM, and 60 GB of disk — a kernel tree with a build directory is 30–40 GB, and you will want two.
Warning: Docker and
podmanare not substitutes. A container shares the host kernel: you can build in one, but you cannotinsmodyour module, you cannot useftracemeaningfully, and/dev/kvmmay not be there. Build in a container if you like; boot in a VM.
Which architecture to target
Build for the architecture your host already is. Cross-compiling is a real skill and it is also a second set of failure modes; add it later, deliberately, not on day one.
| Host arch | Target | Kernel image lands at |
|---|---|---|
| x86-64 | x86-64 | arch/x86/boot/bzImage |
| arm64 | arm64 | arch/arm64/boot/Image |
The lab scripts detect this. When a chapter names an x86 path (arch/x86/entry/entry_64.S), the
arm64 counterpart is named alongside it.
What You Should Already Know
You do not need kernel knowledge. You do need the following, and this curriculum will not re-teach them.
C. Pointers and pointer arithmetic, arrays vs. pointers, struct layout and padding, function
pointers and tables of them, const and its placement, the preprocessor, static and linkage,
integer promotion and overflow, and enough awareness of undefined behavior to be suspicious. You will
read macros that generate functions. You will read container_of(). Neither is explained as C.
Systems fundamentals. Processes and threads, virtual memory and page tables, what an MMU and a TLB do, what a cache line is, interrupts as asynchronous control transfer, DMA as "a device writing to RAM behind your back", and the idea that a syscall is a controlled trap into more-privileged code.
Tooling. git as a working tool, not a save button: rebase -i, log -S, blame, bisect,
format-patch, range-diff. A debugger you can drive (gdb). Comfort at a shell, including
find, xargs, and a grep tool you like — rg (ripgrep) is used throughout this book because the
tree has ~80,000 files and grep -r is a bad time.
Run this self-check on the machine you will work on. If you cannot explain each output in a sentence, spend an evening on that topic first.
# 1. What are these numbers? Which of them is a virtual address and which is not?
cat /proc/self/maps | head -5
cat /proc/self/status | grep -E 'VmSize|VmRSS|Threads'
# 2. Why is the second number smaller, and where did the difference go?
free -m
cat /proc/meminfo | grep -E '^MemTotal|^MemFree|^Cached|^Slab'
# 3. What is this list, and what does the middle column mean?
cat /proc/interrupts | head -8
# 4. Explain the difference between these two, in terms of who did the work.
/usr/bin/time -v ls /usr/bin > /dev/null
/usr/bin/time -v sha256sum /usr/bin/* > /dev/null 2>&1
Required Packages
The build's dependency list is short but unforgiving; a missing libelf shows up as a confusing
error 20 minutes into a build.
# Debian / Ubuntu
sudo apt update && sudo apt install -y \
build-essential flex bison bc libssl-dev libelf-dev libncurses-dev \
git ccache dwarves rsync cpio kmod pahole \
qemu-system-x86 qemu-system-arm gdb \
linux-tools-common linux-tools-generic bpftrace trace-cmd \
clang lld llvm sparse coccinelle exuberant-ctags cscope
# Fedora
sudo dnf install -y \
gcc make flex bison bc openssl-devel elfutils-libelf-devel ncurses-devel \
git ccache dwarves rsync cpio kmod \
qemu-system-x86 qemu-system-aarch64 gdb \
perf bpftrace trace-cmd \
clang lld llvm sparse coccinelle ctags cscope
# Arch
sudo pacman -S --needed base-devel flex bison bc openssl libelf ncurses \
git ccache pahole rsync cpio kmod qemu-full gdb perf bpftrace trace-cmd \
clang lld llvm sparse coccinelle ctags cscope
What each of the less obvious ones is for:
| Package | Why you need it |
|---|---|
libelf-dev / elfutils-libelf-devel | The build's own ELF tooling (objtool). Missing it fails late and confusingly. |
dwarves (provides pahole) | Generates BTF (CONFIG_DEBUG_INFO_BTF). Without it, no CO-RE eBPF against your kernel. |
bc, flex, bison | Kconfig and the build system. Yes, really. |
ccache | Turns a 3-minute incremental rebuild into a 40-second one. Set it up now, not in week four. |
sparse | make C=1 — the kernel's own static checker for __user/__iomem annotations. |
coccinelle | make coccicheck — semantic patches. The tool maintainers use to fix a bug across 300 drivers at once. |
bpftrace, trace-cmd | Observability. The warm-up uses both. |
Verify:
gcc --version && make --version | head -1
qemu-system-x86_64 --version || qemu-system-aarch64 --version
gdb --version | head -1
pahole --version
ccache --version | head -1
rg --version || echo "install ripgrep — this book uses it constantly"
ls -l /dev/kvm && echo "KVM available: QEMU will be fast" || echo "no KVM: boots will be slow but everything works"
Note:
/dev/kvmmissing is not a blocker. Every lab works under TCG emulation; boots take seconds instead of milliseconds. If you are in a cloud VM and want KVM, the provider must have nested virtualization enabled for the instance type.
Getting a Kernel Tree
You want Linus's tree. Not a tarball, not your distro's source package — the git history is a
primary source you will use constantly, and git log, git blame, and git bisect are three of
this curriculum's most-used tools.
mkdir -p ~/kernel && cd ~/kernel
# The canonical mainline tree. ~5 GB and 1.3M+ commits; this takes a while.
git clone --filter=blob:none \
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
git log --oneline -1 # HEAD
make kernelversion # e.g. 6.x.0
git describe --tags # the nearest tag; tells you where you are in the cycle
--filter=blob:none gives you a blobless clone: full history and all commit metadata, with file
contents fetched on demand. It cuts the clone dramatically and git log/git blame/git bisect
still work. If you have the bandwidth and disk, a plain git clone is simpler and never surprises
you offline.
Tip: Clone from
git.kernel.org, not from the GitHub mirror. They have the same commits, but the kernel.org URL is the one everyMAINTAINERST:line names, and matching them up matters when you start adding subsystem trees as remotes in Contribution.
Which commit to work from
| You are doing | Base on |
|---|---|
| Learning, Labs 1–6 | The most recent tag: git checkout -b lab v$(make kernelversion | cut -d. -f1,2) — a released kernel boots reliably |
| Writing a real patch | The subsystem maintainer's tree, on the branch their MAINTAINERS entry names — usually for-next or -next |
| Fixing a bug you found | The oldest tree that still has the bug, then check it forward |
Never base a patch on linux-next. It is an integration artifact with a history that gets rebuilt
daily; a patch against it does not apply anywhere else.
The Setup Gate
You have cleared the gate when all five of these are true. Each maps to a lab step, so doing them now is not wasted work.
[ ] 1. The tree builds. make -j$(nproc) finishes with no errors
[ ] 2. The kernel boots in QEMU. you get a shell inside your own kernel
[ ] 3. GDB attaches and breaks. a breakpoint in the syscall path fires
[ ] 4. Rebuild-and-reboot is fast. under 5 minutes for a one-line change
[ ] 5. You can read the tree. rg finds things; ctags/cscope or an LSP works
Item 4 is the one people skip, and it is the one that decides whether you finish this curriculum. A 40-minute edit-test loop kills curiosity. Fix it now:
# ccache, out-of-tree build dir, and only build what you need.
export CCACHE_DIR=~/.ccache && ccache -M 20G
make O=../build defconfig
make O=../build -j"$(nproc)" CC="ccache gcc"
# ...edit one file...
time make O=../build -j"$(nproc)" CC="ccache gcc" # this is the number that matters
A one-file change in a driver should relink in well under a minute on any modern machine. If it is rebuilding the world, you changed a widely-included header — which is itself worth knowing, and is why maintainers care so much about header hygiene.
Warning:
make localmodconfigbuilds only the modules currently loaded on your machine. It produces a fast build and a kernel that boots on your hardware — and it silently omits everything you were about to experiment with. Usedefconfigfor the labs and reach forlocalmodconfigonly when you know exactly what you are cutting.
The full rig — config fragments, the initramfs, the QEMU invocation, the .gdbinit, and the scripts
that tie them together — is specified in The Lab Rig. Read that before Lab 1.
The Companion Workspace
There is a starter workspace in this repository at book/projects/kernel-labs/. Copy it out and work
in your own repo:
# from the root of this repository:
cp -r book/projects/kernel-labs ~/kernel-labs
cd ~/kernel-labs && git init && git add -A && git commit -m "starter workspace"
cat README.md # what is complete, what is a skeleton, and what it pins
./scripts/build-kernel.sh # config + build, with sane debugging options on
./scripts/mkrootfs.sh # a minimal initramfs
./scripts/run-qemu.sh # boot it, serial console, GDB stub listening
It is scaffolding, not a solution. One module (01-hello) is complete, as a reference to read.
The rest are skeletons whose function bodies are /* TODO(Lab N): ... */, each paired with a KUnit
test or an observable check that fails until you do the work. The failing check is the
specification. Read it, read the lab, implement, re-run.
| Component | State |
|---|---|
scripts/ (build, rootfs, QEMU, GDB, checkpatch) | Complete |
modules/01-hello | Complete — the reference (Lab 2) |
modules/02-chardev … 07-fake-accel | Skeleton + a check (Labs 3+) |
kunit/ | Suites the modules must pass (Lab 6) |
userspace/ | ioctl exercisers, XDP loaders, test programs |
Prefer to start from nothing? Do that instead; the labs give every line. The workspace exists so you are not retyping boilerplate, not so you can skip the thinking.
Note: Out-of-tree modules are not portable across kernel versions — the internal API is not stable, which is the whole point of that argument. The workspace README names the kernel version it is known to build against. If your tree is newer and something does not compile, that is a real lesson, not a broken workspace: find the commit that changed the API with
git log -S'<the function name>'and fix the call site. You will do this for real, in anger, one day.
The Reading Order
flowchart TD
A["Introduction"] --> B["Overview & Prerequisites"]
B --> HG["Hitchhiker's Guide"]
HG --> W["The Warm-Up"]
W --> C["Mental Model — Milestone 0"]
C --> LR["The Lab Rig"]
C --> RM["Roadmap: 15 milestones"]
C --> TM["Teaching Method"]
LR --> F["Foundations<br/>M1–M6"]
TM --> F
F --> CN["Contribution<br/>M7–M10"]
CN --> S["Subsystems<br/>M11"]
S --> E["Engineering<br/>M12"]
E --> CAP["Capstone<br/>M13–M14"]
F -.reference.-> AP["Appendices:<br/>cheat sheets, glossary"]
S -.reference.-> AP
CN -.-> CAP
Foundations before Contribution is not negotiable. You cannot review a patch, or defend your own,
without knowing what a maintainer is actually checking for. But note the dashed line: once
Foundations is done, Contribution and Subsystems can proceed in parallel — send your first trivial
patch while you are reading mm/, because the round-trip latency on the list is measured in weeks
and you want that clock running.
How Long This Takes
Honest estimates, assuming evenings and weekends and that you do the experiments rather than skimming them.
| Part | Calendar time | The part that actually takes the time |
|---|---|---|
| Setup + Warm-up + M0 | 1 week | Getting QEMU + GDB working. Always. |
| Foundations | 5–7 weeks | Not the modules — concurrency and context. Everyone underestimates them. |
| Contribution | 2–3 weeks of work, then waiting | The work is small; the latency is not. Start early. |
| Subsystems | 4–6 weeks | Reading. There is no shortcut and it does not feel like progress until suddenly it does. |
| Engineering | 2 weeks | Bisection is fast; building an honest benchmark is not. |
| Capstone | 4–12 weeks | Not up to you. See the weekly plan. |
If you finish Foundations in a weekend, you read it. Check whether you can answer the validation questions at the end of Context and Atomicity without scrolling up.
A Note on Danger
Kernel code runs with no supervision. There is no segfault that only kills your program, no exception handler that unwinds to a safe point, and nothing between a stray write and someone's data.
| Risk | Mitigation |
|---|---|
| Corrupting a filesystem | Work in a VM with a throwaway disk image. Never mount a real partition read-write in a VM running your kernel. |
| Losing hours to a hang | Boot with a serial console and panic=1; QEMU's monitor can always kill it. |
| Silently corrupting memory | Turn on CONFIG_KASAN, CONFIG_UBSAN, CONFIG_DEBUG_KMEMLEAK, and CONFIG_PROVE_LOCKING for every lab kernel. They are slow. Use them anyway. |
| Shipping a bug upstream | Every check in Style and Checks, every time. |
The debug options are not training wheels. Maintainers ask "did you run this with KASAN and lockdep on?" and the honest answer had better be yes.
Next: The Hitchhiker's Guide to Unix, Linux & the Patch — why the kernel and its community work the way they do. Then The Warm-Up, on the kernel you are already running.