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.

HostStatusWhat actually happens
Linux, x86-64IdealEverything 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, arm64IdealSame, 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 VMYou 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.
WindowsUse WSL2WSL2 is a real Linux kernel with a real userland. Building works. Nested QEMU works but is slower; check /dev/kvm exists.
A cloud VMFine, and often bestA 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 podman are not substitutes. A container shares the host kernel: you can build in one, but you cannot insmod your module, you cannot use ftrace meaningfully, and /dev/kvm may 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 archTargetKernel image lands at
x86-64x86-64arch/x86/boot/bzImage
arm64arm64arch/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:

PackageWhy you need it
libelf-dev / elfutils-libelf-develThe 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, bisonKconfig and the build system. Yes, really.
ccacheTurns a 3-minute incremental rebuild into a 40-second one. Set it up now, not in week four.
sparsemake C=1 — the kernel's own static checker for __user/__iomem annotations.
coccinellemake coccicheck — semantic patches. The tool maintainers use to fix a bug across 300 drivers at once.
bpftrace, trace-cmdObservability. 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/kvm missing 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 every MAINTAINERS T: line names, and matching them up matters when you start adding subsystem trees as remotes in Contribution.

Which commit to work from

You are doingBase on
Learning, Labs 1–6The most recent tag: git checkout -b lab v$(make kernelversion | cut -d. -f1,2) — a released kernel boots reliably
Writing a real patchThe subsystem maintainer's tree, on the branch their MAINTAINERS entry names — usually for-next or -next
Fixing a bug you foundThe 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 localmodconfig builds 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. Use defconfig for the labs and reach for localmodconfig only 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.

ComponentState
scripts/ (build, rootfs, QEMU, GDB, checkpatch)Complete
modules/01-helloComplete — the reference (Lab 2)
modules/02-chardev … 07-fake-accelSkeleton + 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.

PartCalendar timeThe part that actually takes the time
Setup + Warm-up + M01 weekGetting QEMU + GDB working. Always.
Foundations5–7 weeksNot the modules — concurrency and context. Everyone underestimates them.
Contribution2–3 weeks of work, then waitingThe work is small; the latency is not. Start early.
Subsystems4–6 weeksReading. There is no shortcut and it does not feel like progress until suddenly it does.
Engineering2 weeksBisection is fast; building an honest benchmark is not.
Capstone4–12 weeksNot 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.

RiskMitigation
Corrupting a filesystemWork 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 hangBoot with a serial console and panic=1; QEMU's monitor can always kill it.
Silently corrupting memoryTurn 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 upstreamEvery 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.