R0KernelPath
module 095 / 262

Building, Configuring & Installing a Custom Kernel

From `git clone` to a custom kernel booting on your machine — the complete, expert-level workflow every kernel contributor needs.

~12h focused study12 concepts

Overview

Building your own Linux kernel is a rite of passage. It transforms the kernel from an opaque black box into something you personally compiled, configured, and booted — a system where you understand, at least partially, why every decision was made. This module takes you from zero to running a kernel you built yourself.

The Linux kernel source is roughly 30 million lines of C and a few million lines of assembly, spread across thousands of files. The build system — Kbuild — is one of the most sophisticated Makefile-based systems ever written. Understanding it is not just busywork: when you eventually submit a patch, you will need to know how to build only the affected subsystem, how to run checkpatch, and how to test without wiping your existing working kernel.

This module is structured around the actual workflow a kernel developer follows every day: fetch the source, configure it for your specific machine (or a target device), build it, install it, and boot it — all while keeping your machine recoverable if something goes wrong. We will also cover localmodconfig, the single most useful trick for dramatically speeding up kernel builds on a development machine.

One thing to internalize early: the kernel is not a program you run. It is the environment everything else runs inside. Booting a bad kernel means your machine does not come up. This sounds scary, but the solution is simple: always keep a known-good kernel in your bootloader menu. After that, the worst that can happen is a reboot into the old kernel, and you try again. That attitude — "nothing is irreversible, just keep a fallback" — is how kernel developers stay productive.

why this matters for the kernel

Every kernel contribution starts with a local build. You cannot write a patch against code you have never compiled. More concretely: when a bug report comes in, a maintainer will often ask for a bisect — a binary search through git history to find the exact commit that introduced the regression. That process involves dozens of kernel builds. Knowing how to build fast (localmodconfig, ccache, parallel jobs) turns a week-long bisect into an afternoon.

Beyond contribution mechanics, understanding the build and boot process gives you the mental model for everything else in the kernel curriculum. You will see how Kconfig files express dependencies between subsystems, how the .config file is the canonical description of a kernel build, how modules differ from built-in code, and how the bootloader hands control to the kernel. These are not isolated facts — they are the skeleton on which every other kernel concept hangs. A developer who has never built a kernel from source is like a chef who has never been to a kitchen: the theory is thin without the practice.

Kernel Build and Boot Pipeline

rendering diagram…

The complete path from source checkout to a running custom kernel. Note that modules_install must precede make install so that the initramfs builder finds the correct modules.

The concepts

01Getting the Source: kernel.org and the Git Tree

The Linux kernel is developed in a distributed git tree. Linus Torvalds maintains the mainline tree at git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git. There are also subsystem-specific trees (e.g., net-next for networking, drm-tip for graphics) where patches land before being pulled into mainline.

For learning and contribution, clone the mainline Linus tree or the linux-next integration tree (which aggregates all subsystem trees and is the closest thing to "tomorrow's kernel"):

git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
# or the faster mirror:
git clone https://github.com/torvalds/linux.git

The clone is large (~4 GB with full history). If you only need recent history, use --depth=1 to get a shallow clone, which is fine for building but not for git bisect.

Alternatively, download a release tarball from kernel.org:

wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.tar.xz
tar xf linux-6.9.tar.xz
cd linux-6.9

The source tree layout is worth understanding. Top-level directories include: arch/ (architecture-specific code), drivers/ (device drivers — the largest directory), fs/ (filesystems), include/ (header files), kernel/ (core kernel: scheduling, signals, timers), mm/ (memory management), net/ (networking stack), init/ (early boot code), lib/ (small utilities used by the kernel itself), and Documentation/ (technical documentation, often better than any textbook).

Always work in a dedicated directory. Never build in the same tree you use for other projects — .config files are machine-specific.

# Check out a stable branch for a safer build target
git clone --depth=50 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux
git checkout v6.9.3

# Verify you have the required build dependencies (Debian/Ubuntu)
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev

02The Kbuild System and .config: What a Kernel Configuration Is

The kernel is not a fixed binary. It is a parameterized build: whether a given driver, filesystem, or subsystem is included — and whether it is baked into the kernel image or compiled as a loadable module — is controlled by a configuration file called .config in the root of the source tree.

Every configurable option is defined in a Kconfig file somewhere in the tree. For example, drivers/net/ethernet/intel/Kconfig defines E1000E (the Intel Gigabit driver). Each option has a type (bool, tristate, int, hex, string) and can express dependencies on other options.

A tristate option is the key insight: it can be y (yes, built-in to the kernel image), m (module, compiled separately as a .ko file and loaded on demand), or n (not compiled at all). This is what makes Linux so flexible — a desktop kernel might have 2,000 modules; an embedded kernel might have zero.

The .config file is just a text file:

CONFIG_E1000E=m
CONFIG_BLK_DEV_SD=y
CONFIG_EXT4_FS=y
CONFIG_DEBUG_KERNEL=n
# CONFIG_LOCALVERSION_AUTO is not set

Kbuild reads .config and generates include/generated/autoconf.h, which kernel C files include via #include <linux/config.h>. Every #ifdef CONFIG_SOMETHING in the kernel source is testing a value from this file.

The Kbuild system (the collection of Makefiles and Kconfig scripts) figures out what to build, in what order, with what flags. It handles incremental builds correctly: changing one .c file only rebuilds that file and links. Changing .config triggers selective rebuilds of affected parts.

# See what CONFIG_E1000E means after building:
grep E1000E .config
# CONFIG_E1000E=m

# The autoconf.h it generates:
grep E1000E include/generated/autoconf.h
# #define CONFIG_E1000E_MODULE 1

# In driver source, this is tested like:
# #ifdef CONFIG_E1000E
# ...compile this code...
# #endif

03make defconfig, allnoconfig, and Picking a Starting Config

You never write .config from scratch. You start from a baseline and customize. There are several baselines:

make defconfig generates a .config with the defaults defined in arch/<arch>/configs/<arch>_defconfig. On x86_64, this is a reasonable generic configuration for a PC. On ARM, you might have dozens of board-specific defconfigs (e.g., make bcm2835_defconfig for Raspberry Pi). This is the most common starting point for learning.

make allnoconfig sets every option to n (or its minimum). The result is a kernel that barely boots. Useful for embedded developers who want to add only exactly what they need.

make allyesconfig sets everything to y. Do not actually build this — it takes hours and the result is massive.

make localmodconfig — discussed in its own concept — reads the currently loaded modules from your running system and disables everything your machine does not use. This is the most practical shortcut for developer machines.

Using your distro's config is often the safest starting point for a machine you want to keep working:

cp /boot/config-$(uname -r) .config
make olddefconfig   # answer new questions with defaults

make olddefconfig is important: whenever you pull new kernel source onto an old .config, there will be new Kconfig options the old config doesn't know about. olddefconfig silently accepts the default for every new option. oldconfig asks you interactively, which can be tedious for hundreds of new options.

The ARCH variable controls the target architecture:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

This lets you build a kernel for a Raspberry Pi from an x86 laptop.

# Start from architecture defaults
make defconfig

# Or copy your running kernel's config
cp /boot/config-$(uname -r) .config
make olddefconfig   # promote to current Kconfig schema

# Cross-compile for ARM64 (e.g. Raspberry Pi 4)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

04make menuconfig: Interactive Configuration with a TUI

make menuconfig is the standard interactive configurator. It presents a terminal-based menu (using ncurses) that lets you navigate the entire Kconfig tree, read the help text for every option, and toggle settings with Y, M, or N.

The interface looks like a tree of nested menus. You navigate with arrow keys, Enter to descend, Escape to go back, ? to read the help text for the selected option (always read the help — it is often better documented than any external source), Space to toggle, / to search.

Searching is key. Press /, type E1000E, and you will see every option whose name or help contains that string, along with the full path to find it in the menu and its current value.

Alternatives to menuconfig:

  • make xconfig — Qt-based GUI, nicer but requires Qt dev libraries
  • make nconfig — newer ncurses frontend, slightly cleaner UI
  • make config — line-by-line prompts, brutal for large configs, use only in scripts

Critical rule: After running menuconfig, always check what changed:

git diff .config

or compare against a known-good config with scripts/diffconfig old_config .config. Accidentally enabling CONFIG_DEBUG_KMEMLEAK or CONFIG_KASAN will make your kernel painfully slow. Accidentally disabling CONFIG_EXT4_FS means you cannot mount your root filesystem.

The LOCALVERSION option (General Setup -> Local version) is worth setting to something like -myfirstkernel. This appends to the kernel version string and appears in uname -r. It helps you know which kernel you built and keeps your custom kernel from conflicting with distro-installed kernels.

# Launch the TUI configurator
make menuconfig

# After editing, see what you changed vs the distro config
diff /boot/config-$(uname -r) .config | grep '^[<>]' | head -40

# Or use the kernel's own diff script
scripts/diffconfig /boot/config-$(uname -r) .config

05localmodconfig: The Developer's Secret Weapon

make localmodconfig is the single highest-leverage trick for kernel developers on their own hardware. Here is what it does:

  1. It reads the list of currently loaded modules from /proc/modules (the modules your running kernel has loaded right now).
  2. For every module that is not loaded, it sets the corresponding CONFIG_* option to n.
  3. The result is a .config that only builds drivers and features your machine actually uses.

On a typical workstation, a full make defconfig configuration might have 5,000+ modules selected. After localmodconfig, you might have 200. Build time drops from 60+ minutes to under 10 minutes. This is transformative for development velocity.

The critical caveat: it captures only the modules loaded right now. If you run localmodconfig in a minimal boot (no USB drive inserted, no Bluetooth active, etc.), the resulting kernel will not have those drivers. The usual advice:

  • Plug in every device you might want to use before running localmodconfig
  • Mount every filesystem type you use
  • Start every service that might load modules

Another caveat: some features are not modules at all — they might be y in your running config but since they were never a module, localmodconfig cannot infer them from /proc/modules. Always use your running distro's config as the input to localmodconfig, not defconfig:

cp /boot/config-$(uname -r) .config
make localmodconfig   # strips unused modules from the copied config

If you are building for a different machine or a VM, you can capture that machine's module list:

ssh target-machine lsmod > /tmp/target-mods
LMOD=/tmp/target-mods make localmodconfig
# The complete localmodconfig workflow
cp /boot/config-$(uname -r) .config

# Make sure everything you want is loaded right now:
modprobe bluetooth  # if you use Bluetooth
# plug in USB devices, mount all filesystem types, etc.

make localmodconfig  # will ask about any new Kconfig symbols

# Check the dramatic reduction in selected modules:
grep '=m' /boot/config-$(uname -r) | wc -l   # e.g. 2847
grep '=m' .config | wc -l                      # e.g. 193

06Building the Kernel: make, Parallel Jobs, and ccache

Once you have a .config, building is a single command:

make -j$(nproc)

The -j flag specifies parallel jobs. $(nproc) expands to the number of logical CPU cores. On an 8-core machine, -j8 gives roughly 8x speedup over a serial build. Some people use -j$(nproc) or even -j$(($(nproc) + 2)) to slightly over-subscribe.

What the build produces:

  • arch/x86/boot/bzImage — the compressed kernel image ("big zImage"). This is what the bootloader loads.
  • vmlinux — the uncompressed ELF kernel image in the root of the source tree. This is used for debugging with gdb, crash dump analysis with crash, and generating System.map.
  • System.map — a symbol table mapping kernel function/variable names to their virtual addresses. Essential for debugging kernel oopses.
  • *.ko files scattered throughout the tree — compiled kernel modules.

ccache — the compiler cache — dramatically speeds up repeated builds, especially after make clean or when switching branches:

export KBUILD_BUILD_HOST=mymachine
make CC="ccache gcc" -j$(nproc)

After the first full build, ccache caches compilation outputs. A rebuild of an unchanged tree takes under 30 seconds instead of 10 minutes.

Out-of-tree builds keep the source tree clean:

make O=/path/to/build/dir -j$(nproc)

All build artifacts go to the output directory, leaving the source tree pristine. Kernel developers who work on multiple configs simultaneously use this extensively.

Compiler selection: The kernel can be built with GCC or Clang (the LLVM-based compiler). Clang builds are increasingly supported and required for some sanitizers:

make CC=clang LLVM=1 -j$(nproc)

Expect build times: with localmodconfig and ccache on a modern 8-core CPU, roughly 3-8 minutes. A full defconfig from scratch, first build: 30-90 minutes depending on hardware.

# Standard build
make -j$(nproc)

# With ccache for fast rebuilds
make CC="ccache gcc" -j$(nproc) 2>&1 | tee build.log

# Out-of-tree build (source stays clean)
mkdir -p /tmp/kbuild
make O=/tmp/kbuild -j$(nproc)

# Build only a specific subdirectory (e.g. just the e1000e driver)
make -j$(nproc) drivers/net/ethernet/intel/e1000e/

# Verbose build (see actual compiler commands)
make V=1 -j$(nproc) 2>&1 | head -100

07Installing Modules: make modules_install

Kernel modules are .ko files built alongside the kernel image. They must be installed to a specific path so the running kernel can find and load them.

The modules are installed under /lib/modules/<kernel-version>/, where <kernel-version> is the output of uname -r for the new kernel. For example: /lib/modules/6.9.3-myfirstkernel/.

Installing modules:

sudo make modules_install

This copies all .ko files to /lib/modules/$(make kernelversion)$(CONFIG_LOCALVERSION)/, runs depmod to build the module dependency database (modules.dep), and sets up the directory structure the kernel expects.

INSTALL_MOD_PATH lets you install to an alternate root, which is essential for cross-compilation and for building filesystem images:

sudo make INSTALL_MOD_PATH=/mnt/sdcard modules_install

depmod is worth understanding. When you run modprobe bluetooth, the kernel does not search every .ko file — it consults /lib/modules/$(uname -r)/modules.dep, which lists every module's dependencies. This file is built by depmod. If you manually copy a .ko without running depmod, modprobe may fail. make modules_install handles this automatically.

After modules_install, verify:

ls /lib/modules/6.9.3-myfirstkernel/kernel/drivers/net/ethernet/intel/e1000e/
# e1000e.ko.zst

Modern kernels use zstd compression for modules (.ko.zst). The modprobe tool handles decompression transparently.

# Install modules (requires root)
sudo make modules_install

# Check what was installed
ls /lib/modules/$(make kernelversion)/
# build  kernel  modules.alias  modules.dep  modules.order  ...

# Verify depmod ran correctly
cat /lib/modules/$(make kernelversion)/modules.dep | head -5

# Cross-compile install to a mounted SD card at /mnt/rootfs
sudo make ARCH=arm64 INSTALL_MOD_PATH=/mnt/rootfs modules_install

08Installing the Kernel Image: make install and manual methods

Installing the kernel image itself means placing it where the bootloader can find it — typically /boot/ — and updating the bootloader configuration.

make install is the official method. It calls arch/x86/boot/install.sh (or the arch-equivalent), which copies the kernel image, System.map, and .config to /boot/, then calls the installed installkernel script (often provided by your distro) which updates the bootloader:

sudo make install

On Debian/Ubuntu, this typically:

  1. Copies arch/x86/boot/bzImage to /boot/vmlinuz-<version>
  2. Copies System.map to /boot/System.map-<version>
  3. Copies .config to /boot/config-<version>
  4. Runs update-initramfs -c -k <version> to build the initial ramdisk
  5. Runs update-grub to regenerate the GRUB menu

The initramfs (initial RAM filesystem) deserves explanation. Modern Linux systems use a two-phase boot: the kernel boots with a minimal ramdisk that contains just enough to mount the real root filesystem (including drivers for encrypted disks, LVM, RAID, etc.). The initramfs is built by initramfs-tools or dracut, and it must include the modules your system needs during early boot. This is why make modules_install must happen before make install — the initramfs build tool will package modules from /lib/modules/<new-version>/.

Manual installation (when make install doesn't work for your setup):

sudo cp arch/x86/boot/bzImage /boot/vmlinuz-6.9.3-myfirstkernel
sudo cp System.map /boot/System.map-6.9.3-myfirstkernel
sudo cp .config /boot/config-6.9.3-myfirstkernel
# Build initramfs
sudo update-initramfs -c -k 6.9.3-myfirstkernel
# Regenerate GRUB
sudo update-grub

For ARM/embedded targets, you often install differently:

# Copy to SD card
sudo cp arch/arm64/boot/Image /mnt/boot/
sudo cp arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb /mnt/boot/
# Full installation sequence (do modules FIRST)
sudo make modules_install      # step 1: install .ko files
sudo make install              # step 2: install image + regenerate initramfs + update grub

# Verify the files are in /boot
ls -lh /boot/vmlinuz* /boot/initrd* /boot/System.map*

# Check GRUB found the new kernel
grep 'menuentry' /boot/grub/grub.cfg | head -10

09The Bootloader: GRUB2 and Booting Your Kernel

The bootloader is the first software that runs after the BIOS/UEFI firmware. On most x86 Linux systems, this is GRUB2 (Grand Unified Bootloader, version 2). GRUB's job is simple but critical: load the kernel image and initramfs into memory, pass a command line to the kernel, and hand over control.

How GRUB2 works:

  1. GRUB's first stage (Stage 1) lives in the MBR or EFI partition — it is tiny (446 bytes for MBR)
  2. Stage 2 is a larger program loaded by Stage 1 that can read filesystems and display the boot menu
  3. GRUB reads /boot/grub/grub.cfg (auto-generated by update-grub / grub-mkconfig) to build the menu
  4. You select a kernel entry; GRUB loads vmlinuz-* and initrd.img-* into RAM
  5. GRUB calls the kernel's protected-mode entry point with the kernel command line

update-grub scans /boot/ for files matching vmlinuz-* and automatically generates menu entries. This is why make install triggers it.

The boot menu is your safety net. In GRUB2, press Escape (or hold Shift on some configs) during boot to see the menu. You should see your new kernel and your old distro kernel. Always keep the old kernel. Never delete a working kernel until you have verified the new one boots.

The kernel command line is passed from GRUB to the kernel. You can see it for the running kernel:

cat /proc/cmdline
# BOOT_IMAGE=/vmlinuz-6.9.3-myfirstkernel root=/dev/sda1 ro quiet splash

Key parameters: root= specifies the root device, ro mounts it read-only initially (the init system remounts rw), quiet suppresses verbose boot messages, console=ttyS0,115200 redirects console output to a serial port (useful for debugging).

For UEFI systems, grub-install and update-grub behave similarly but the EFI partition stores GRUB as a .efi application. The principle is identical.

# Regenerate GRUB config after installing a new kernel
sudo update-grub    # Debian/Ubuntu/Mint
# sudo grub2-mkconfig -o /boot/grub2/grub.cfg   # Fedora/RHEL

# See all kernels GRUB knows about
grep 'menuentry' /boot/grub/grub.cfg

# Check current kernel's command line
cat /proc/cmdline

# Temporarily edit the command line at boot:
# In GRUB menu: press 'e' to edit, find the 'linux' line,
# append 'init=/bin/bash' for emergency shell
# Press Ctrl+X to boot with the modified line

10Verifying the Boot: uname, dmesg, and Recovery

After installation, reboot and select your new kernel from the GRUB menu. When the system comes up, verify:

uname -r
# 6.9.3-myfirstkernel

If the version includes your LOCALVERSION string, you are running your kernel. Congratulations — that is a milestone.

dmesg is the kernel ring buffer — a circular log of messages the kernel wrote during and after boot. This is your primary debugging tool for kernel issues:

dmesg | head -50    # early boot messages
dmesg | grep -i error
dmesg | grep -i "failed to"

Look for lines tagged [ 0.000000] — these are from very early boot. Hardware initialization messages (PCIe, USB, storage controllers) appear within the first few seconds.

Kernel panics — if the kernel panics during boot (usually due to not finding the root filesystem, missing a critical driver, or a genuine bug), you will see a panic message. Common causes:

  • Missing ext4 support (you set CONFIG_EXT4_FS=n while your root is ext4)
  • Missing storage controller driver
  • Initramfs not built, or built before modules were installed
  • Wrong root device on command line

Recovery procedure:

  1. Reboot, press Escape/Shift in GRUB to get the menu
  2. Select the previous known-good kernel
  3. Boot normally
  4. Investigate: check dmesg, check /boot/ for correct files, rebuild initramfs

Debugging boot failures with serial console: On physical hardware, add console=ttyS0,115200n8 to the kernel command line to redirect boot messages to a serial port. On VMs (QEMU), use -serial stdio and console=ttyS0.

A less common but important tool: CONFIG_EARLY_PRINTK / earlyprintk=serial,ttyS0,115200 enables kernel messages before the full console driver is initialized — essential for debugging very early panics.

# After booting the new kernel:
uname -a
# Linux hostname 6.9.3-myfirstkernel #1 SMP Tue Jun  4 12:00:00 UTC 2026 x86_64 GNU/Linux

# Check for any errors in kernel log
dmesg | grep -iE '(error|fail|warn)' | head -30

# See all loaded modules
lsmod | wc -l

# Check your custom module was loaded (if you built one)
modinfo e1000e | grep version

# If you need to rebuild initramfs after a failed boot:
sudo update-initramfs -u -k 6.9.3-myfirstkernel

11Cleaning the Build Tree and Managing Multiple Configs

As a kernel developer, you will maintain multiple configurations — one for your workstation, one for an embedded target, one with debug options enabled. Understanding the clean targets and out-of-tree builds keeps you sane.

Clean targets:

  • make clean — removes most build artifacts but keeps .config. Use this when you want to rebuild after source changes.
  • make mrproper — removes everything including .config and generated headers. Equivalent to a fresh checkout. Use before switching to a completely different config.
  • make distclean — mrproper plus editor backup files. Rarely needed.

Out-of-tree builds (O= flag) are the cleanest solution for multiple configs:

mkdir -p ~/kbuilds/debug ~/kbuilds/fast
# Configure each separately
make O=~/kbuilds/debug menuconfig
make O=~/kbuilds/fast menuconfig
# Build each separately
make O=~/kbuilds/debug -j$(nproc)
make O=~/kbuilds/fast -j$(nproc)

The source tree is shared (read-only during build); each output directory has its own .config, objects, and final image.

Saving and restoring configs:

make savedefconfig   # saves minimal defconfig to defconfig (only non-default options)
cp defconfig my_config_backup
# Later:
cp my_config_backup .config
make olddefconfig

savedefconfig is particularly useful because it produces a minimal config (only the options that differ from defaults), making it easy to diff between configurations and commit to version control.

For kernel development, it is common to keep a configs/ directory in your notes with saved configs for each machine and build purpose. Document what each config is for — your future self will thank you.

# Keep source clean, build in separate directory
make O=~/kbuild-debug menuconfig
make O=~/kbuild-debug -j$(nproc)
sudo make O=~/kbuild-debug modules_install
sudo make O=~/kbuild-debug install

# Save a minimal representation of the config
make O=~/kbuild-debug savedefconfig
cp ~/kbuild-debug/defconfig ~/my-configs/debug-6.9.defconfig

# Full clean to start over
make mrproper

12Building in a VM: QEMU as a Kernel Development Environment

Building and testing a kernel in a QEMU virtual machine is safer and faster for learning than doing it on your main machine. Panics are inconsequential — you just restart the VM. You can also test extreme configs that would be dangerous on real hardware.

The minimal QEMU kernel development workflow requires:

  1. A kernel built with CONFIG_KVM_GUEST=y (or just use make defconfig)
  2. A root filesystem (a minimal Debian or Ubuntu install, or a buildroot rootfs)
  3. A QEMU invocation that passes the kernel directly (bypassing the guest bootloader)
# QEMU can boot a kernel directly without a bootloader:
qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -initrd /path/to/initramfs.img \
  -append "root=/dev/sda console=ttyS0" \
  -hda /path/to/rootfs.img \
  -m 1G \
  -nographic   # use serial console, no GUI

The -kernel flag tells QEMU to load the kernel directly — this completely bypasses GRUB and lets you test a new kernel in under 5 seconds. This is how almost all kernel CI systems work.

virtme-ng (formerly virtme) is a tool that automates this even further — it boots your kernel directly from the build directory using your host system's files as the rootfs:

pip install virtme-ng
virtme-ng --installed-kernel  # boot the installed kernel
virtme-ng --kdir .             # boot the kernel in the current source tree

This is arguably the best tool for kernel development iteration — edit, build, virtme-ng --kdir ., test, all in under a minute.

For networking tests, add -net nic,model=virtio -net user to QEMU. For debugging with GDB, add -s -S (listen on port 1234, pause at startup) and connect with gdb vmlinux -ex 'target remote :1234'.

# Build a minimal kernel for QEMU testing
make defconfig
# Enable virtio drivers for QEMU paravirtualization
scripts/config --enable CONFIG_VIRTIO_PCI
scripts/config --enable CONFIG_VIRTIO_BLK
scripts/config --enable CONFIG_VIRTIO_NET
scripts/config --enable CONFIG_9P_FS
make olddefconfig
make -j$(nproc)

# Boot it (assuming you have a rootfs)
qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -append "root=/dev/vda rw console=ttyS0" \
  -drive file=rootfs.img,format=raw,if=virtio \
  -m 512M -nographic

Key insights

  • ✦The .config file is the complete specification of your kernel build — every ifdef in the kernel source traces back to a line in this file. Understanding it is understanding the kernel.
  • ✦Always do modules_install BEFORE make install: the initramfs build tool packages modules from /lib/modules/, and it must find them already installed.
  • ✦localmodconfig is not a shortcut — it is the correct way to build a kernel for a specific machine. Full defconfig includes thousands of drivers for hardware you will never have.
  • ✦A kernel panic on first boot is not a failure — it is a debugging exercise. Keep the old kernel in GRUB, diagnose with dmesg and the panic message, fix the config, rebuild.
  • ✦Out-of-tree builds (make O=/path) let you maintain multiple configurations against the same source tree — essential once you are testing patches against different architectures or debug levels.
  • ✦QEMU with -kernel bypasses the bootloader entirely, reducing the edit-build-test cycle to minutes. This is how professional kernel developers work.
  • ✦The kernel version string (from uname -r) includes your CONFIG_LOCALVERSION. Set it to something meaningful — it is how you distinguish your builds and how module paths are organized.
  • ✦savedefconfig produces a minimal config (only non-default options) that is human-readable, diff-able, and version-controllable. Always save configs you care about this way.

Common pitfalls

  • ⚠Installing the kernel image BEFORE modules_install: the initramfs builder will package an empty or wrong module directory, causing a boot failure with 'no root device found'.
  • ⚠Using make defconfig on a machine with an exotic root filesystem (btrfs, LVM, LUKS): the defconfig may not include drivers needed to mount root. Always start from cp /boot/config-$(uname -r) .config instead.
  • ⚠Running localmodconfig in a minimal session (fresh login, no devices plugged in): you will miss USB, Bluetooth, secondary filesystem drivers. Load everything first.
  • ⚠Setting CONFIG_LOCALVERSION to a string that already exists in /lib/modules: modules from two different builds collide. Always use a unique localversion string per build.
  • ⚠Forgetting to update-grub after make install: the new kernel files are in /boot but GRUB does not know about them. The machine boots the old kernel and the developer is confused.
  • ⚠Deleting the old distro kernel's entry from GRUB before verifying the new kernel works: if the new kernel panics, you have no recovery path without a live USB.
  • ⚠Not building with enough parallelism: make without -j runs single-threaded and can take 3-5x longer. Always use make -j$(nproc).
  • ⚠Confusing vmlinux (uncompressed ELF in source root, for debugging) with bzImage (the compressed bootable image in arch/x86/boot/). Only bzImage goes to /boot.

Practice

  1. 01
    easy

    Build a kernel using your distro's config as a starting point. Set CONFIG_LOCALVERSION to '-student01', run make localmodconfig, then compare the number of =m entries in the original config vs your new config. Document the reduction ratio.

  2. 02
    medium

    Install your newly built kernel in a QEMU VM (using the -kernel flag to bypass GRUB entirely). Build a minimal rootfs using debootstrap or download a pre-built one. Boot it and verify uname -r shows your custom version string.

  3. 03
    medium

    Perform a kernel bisect. Clone the stable kernel tree, then intentionally 'break' a build by finding a commit between two versions that changed a driver you care about (use git log --oneline drivers/net/). Practice git bisect good / git bisect bad until you identify the exact commit. You do not need to actually boot each intermediate kernel — just verify each one compiles without error.

  4. 04
    hard

    Build the kernel with CONFIG_KASAN=y (Kernel Address Sanitizer) and CONFIG_DEBUG_KERNEL=y enabled, boot it in QEMU, and write a tiny kernel module that deliberately accesses one byte past the end of a kmalloc'd buffer. Observe the KASAN report in dmesg.

  5. 05
    medium

    Cross-compile a kernel for ARM64 on an x86 machine. Use make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig followed by make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc). Verify the resulting image is an AArch64 ELF: file vmlinux.

  6. 06
    easy

    Use make savedefconfig to save a minimal representation of your localmodconfig-trimmed .config. Commit it to a personal git repository. Then restore it on a fresh kernel checkout using cp saved_defconfig .config && make olddefconfig, and verify the kernel builds identically.

Resources

Checkpoints — you can now…

  • ✓You can clone the Linux kernel source, apply make localmodconfig starting from your distro's config, and explain what the reduction in module count means.
  • ✓You can navigate make menuconfig, find any config option using /, read its help text, and understand the difference between y, m, and n for a tristate option.
  • ✓You can build a complete kernel with make -j$(nproc), identify bzImage vs vmlinux, and explain what each file is used for.
  • ✓You can perform the full installation sequence in the correct order (modules_install then install), regenerate the initramfs, and update GRUB.
  • ✓You can boot your custom kernel and verify it with uname -r, then diagnose basic boot failures using dmesg and GRUB's recovery menu.
  • ✓You can set up an out-of-tree build with make O=..., save a minimal config with make savedefconfig, and restore it on a fresh checkout.
  • ✓You can boot a custom kernel directly in QEMU using the -kernel flag, bypassing the bootloader, and understand why this speeds up the development cycle.