Make & the Kernel Build System (Kbuild/Kconfig)
Master GNU Make from first principles, then climb to the Linux kernel's Kbuild/Kconfig system and build your first kernel image and loadable module from source.
Overview
Every piece of software you have ever run was assembled from source code into machine instructions by a build system. For a project as large as the Linux kernel — roughly 30 million lines of C, assembly, and headers spread across thousands of files — that build system is not a luxury. It is the skeleton that holds the whole organism together.
This module is about two intertwined systems: GNU Make (the general-purpose build automation tool that has been the backbone of Unix software since 1976) and Kbuild/Kconfig (the Linux kernel's own layer built on top of Make, which handles configuration, conditional compilation, and the orchestration of a build that can target dozens of different CPU architectures and hundreds of thousands of feature combinations).
We will start from zero: why do we even need a build tool? What is a Makefile and how does it think? Then we climb steadily toward the real thing — running make menuconfig, understanding what a defconfig is, learning how Kconfig turns your choices into C preprocessor symbols, and finally seeing exactly how a bootable kernel image like vmlinux, bzImage, or Image is produced from source.
By the end of this module you will be able to read a real kernel Makefile, understand every phase of the kernel build, reconfigure the kernel for a custom purpose, and build an out-of-tree kernel module. These are not just theoretical skills — they are the literal first steps every kernel contributor takes when they sit down to touch kernel code.
You cannot contribute to the Linux kernel without understanding how it is built. Every patch you write must compile cleanly. Every driver you add must be wired into Kconfig so users can enable or disable it. Every time a maintainer applies your patch, the CI system re-runs the build across a dozen architectures — and if your change breaks any of them, your patch gets rejected. Understanding Kbuild means you understand how your code fits into the larger organism.
More fundamentally, the kernel build system is a masterclass in large-scale software engineering. The techniques you learn here — incremental builds, dependency tracking, conditional compilation via a configuration language, cross-compilation for foreign architectures — appear in every serious systems project. Rust's Cargo, the Android build system, ChromiumOS's portage, and even complex JavaScript monorepo tools are all solving the same core problem that Make was designed for in 1976. Learning it at the source, on the world's most important open-source project, gives you an intuition that transfers everywhere.
Kernel Build Pipeline: From .config to Boot Image
The kernel build pipeline: user configuration choices flow through Kconfig into .config, which drives both the C preprocessor (via autoconf.h) and Kbuild's decision of what to compile. Object files are linked into vmlinux, then packaged into an architecture-specific boot image. Modules are linked separately and installed into /lib/modules/.
The concepts
01Why Build Systems Exist: The Dependency Problem
Imagine you have 10,000 C source files. You change one of them. Which object files need to be recompiled? Which need to be re-linked? If you recompile everything every time, a small edit might take 20 minutes. If you recompile nothing, your binary might be inconsistent and crash in mysterious ways.
This is the dependency problem: given a change to some set of inputs, compute the minimal set of outputs that must be regenerated.
A build system solves this by maintaining a dependency graph — a directed acyclic graph (DAG) where each node is a file (source, object, header, or binary) and each edge means 'this file depends on that file'. When you run the build, the system compares timestamps: if an output file is older than any of its inputs, it must be regenerated. Otherwise it can be skipped.
The genius is that this is compositional: if foo.o depends on foo.c and foo.h, and foo.h changed, then foo.o must be regenerated. If libfoo.a depends on foo.o, then libfoo.a must also be regenerated. The build system follows the chain automatically.
This is exactly what make does, and it is exactly why the Linux kernel build takes 20 seconds for a small change instead of 40 minutes.
# Without a build system, the naive approach:
# Recompile everything every time:
# gcc -c foo.c bar.c baz.c && gcc -o app foo.o bar.o baz.o
# This is O(n) compile time per change.
# With a build system, only changed files are recompiled.
# The build system uses file timestamps to decide what to redo.
02GNU Make: Rules, Targets, and Prerequisites
GNU Make reads a file called Makefile (or GNUmakefile) and executes it. The fundamental unit of a Makefile is the rule:
target: prerequisites
recipe
- target: the file to be produced (or a phony name like
allorclean). - prerequisites: files that must exist and be up-to-date before the recipe runs. If any prerequisite is newer than the target, or the target does not exist, the recipe runs.
- recipe: shell commands to produce the target. Critically, each line of a recipe must start with a TAB character, not spaces. This is one of Make's most infamous quirks.
Make evaluates rules lazily: it starts from the goal (usually the first target in the file, or the one you specify on the command line) and works backward through prerequisites, building a dependency tree. It then executes leaves first, working toward the goal.
Variables in Make are set with = (lazily expanded — evaluated when used) or := (immediately expanded — evaluated when defined). The difference matters in complex Makefiles. You reference a variable with $(VAR).
Automatic variables are especially useful:
$@— the name of the target$<— the first prerequisite$^— all prerequisites$*— the stem of a pattern rule match
Pattern rules let you write generic rules. %.o: %.c means 'to build any .o file, compile the corresponding .c file'. This is how a real project avoids writing one rule per source file.
# A minimal but complete Makefile
CC := gcc
CFLAGS := -Wall -O2
# Pattern rule: build any .o from the corresponding .c
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# Link rule
app: main.o util.o
$(CC) -o $@ $^
# Phony target: doesn't correspond to a real file
.PHONY: clean
clean:
rm -f *.o app
03Automatic Dependency Generation
Pattern rules handle the easy case. But what about header files? If main.c includes util.h and util.h changes, main.o must be recompiled — but the simple pattern rule %.o: %.c doesn't know about util.h.
The solution is automatic dependency generation. GCC (and Clang) can emit a .d file alongside each .o file that lists all the headers that were included during compilation. You then include all those .d files in your Makefile so Make learns about the header dependencies automatically.
The flag -MMD tells GCC to generate a dependency file without including system headers. -MP adds phony targets for each header, preventing errors when a header is deleted.
This is exactly what Kbuild does internally. The kernel's build system generates .foo.o.d files for every object, and includes them to track header dependencies across the entire tree.
This is a beautiful example of using the build system to bootstrap its own dependency graph.
CC := gcc
CFLAGS := -Wall -MMD -MP
SRCS := main.c util.c
OBJS := $(SRCS:.c=.o)
DEPS := $(SRCS:.c=.d)
app: $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# Include the auto-generated dependency files
# The '-' prefix means: don't error if they don't exist yet
-include $(DEPS)
.PHONY: clean
clean:
rm -f $(OBJS) $(DEPS) app
04Kbuild: The Kernel's Build System Layer
The Linux kernel's build system is called Kbuild. It is not a replacement for Make — it is a sophisticated, convention-based layer on top of Make. Kbuild provides:
-
A uniform
Makefileconvention for every directory in the tree. Each subdirectory has aMakefile(sometimes calledKbuild) that only needs to say which objects to build, not how to build them. The 'how' is centralized inscripts/Makefile.buildand related files. -
Integration with Kconfig. Kbuild reads the
.configfile (produced by Kconfig) and makes itsCONFIG_*symbols available as Make variables. IfCONFIG_USB=m, Kbuild knows to build the USB subsystem as a module. -
Cross-architecture support. The same
makeinvocation, withARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-, builds for a completely different CPU. The architecture-specific logic lives inarch/<arch>/Makefile.
The key conventions in a directory-level Kbuild Makefile are:
obj-y += foo.o— compilefoo.cand link it statically into the kernelobj-m += bar.o— compilebar.cas a loadable moduleobj-$(CONFIG_FOO) += foo.o— include ifCONFIG_FOO=y, as module ifCONFIG_FOO=m, skip ifCONFIG_FOO=nfoo-objs := a.o b.o—foo.kois built from multiple source files
This is how a simple three-line file in drivers/usb/core/Makefile can correctly build or skip the entire USB core depending on configuration.
# A real-world-style directory Makefile (e.g. drivers/mydriver/Makefile)
# Build mydriver as a module if CONFIG_MYDRIVER=m,
# or link it in if CONFIG_MYDRIVER=y
obj-$(CONFIG_MYDRIVER) += mydriver.o
# mydriver.ko is composed of two source files
mydriver-objs := mydriver_core.o mydriver_hw.o
# A helper library, always built if we're building the driver
obj-$(CONFIG_MYDRIVER) += mydriver_lib.o
05Kconfig: The Configuration Language
Kconfig is a domain-specific language for describing configuration options — the 'what can be configured' layer. It answers: what choices exist, what are their types, what are their defaults, and what depends on what?
Every subsystem has a Kconfig file. The root Kconfig sources them all recursively. When you run make menuconfig, a tool reads all these Kconfig files and presents you with a menu-driven interface for setting your choices.
Key Kconfig constructs:
bool: An option that is eithery(built-in) orn(excluded).tristate: An option that can bey(built-in),m(module), orn(excluded). Most driver options are tristate.int/hex/string: For numeric or string parameters.depends on: This option is only available if the named option is enabled. This enforces a dependency DAG over the configuration space.select: If this option is enabled, automatically enable the named option. Use sparingly — it can create cycles.default: The default value if the user doesn't set it explicitly.help: Documentation string shown in menuconfig.
After running menuconfig (or any config frontend), the result is written to .config in the kernel source tree. Kconfig also generates include/generated/autoconf.h, which translates every CONFIG_FOO=y into #define CONFIG_FOO 1 and every CONFIG_FOO=m into #define CONFIG_FOO_MODULE 1. This is how C code does #ifdef CONFIG_USB — it is just reading a generated header.
# An example Kconfig entry (e.g. in drivers/mydriver/Kconfig)
config MYDRIVER
tristate "My Example Hardware Driver"
depends on PCI && HAS_IOMEM
select DMA_ENGINE
default n
help
This driver supports the fictional MyHardware PCI device.
If unsure, say N.
config MYDRIVER_DEBUG
bool "Enable debug output for MyDriver"
depends on MYDRIVER
default n
help
Enables verbose debug logging. This has a performance cost
in production builds. Say N unless debugging.
06Configuration Frontends: menuconfig, defconfig, and .config
The .config file is the single source of truth for a kernel build's configuration. It is a text file — one line per option — that looks like:
CONFIG_USB=y
CONFIG_USB_STORAGE=m
# CONFIG_USB_SERIAL is not set
You interact with .config through several frontends:
make menuconfig: A full-screen ncurses TUI. Browse, search (/), and toggle options. This is the most common interactive method. The equivalent GUI ismake xconfig(Qt) ormake gconfig(GTK).make defconfig: Creates a default.configfor the current architecture fromarch/<arch>/configs/<arch>_defconfig. This is the standard starting point for a new build.make ARCH=arm64 defconfig: Creates a default config for ARM64 (which lives atarch/arm64/configs/defconfig).make allmodconfig: Enables every option that can be a module, as a module. Useful for build-coverage testing.make allyesconfig: Enables everything as built-in. The resulting kernel is enormous.make tinyconfig: Absolute minimum kernel. Good for understanding what the smallest possible kernel looks like.make oldconfig: Given an existing.configthat may be outdated (e.g. from an older kernel version), prompts you only for the new options that were added. Essential when upgrading.make savedefconfig: Saves a minimal defconfig (only non-default values) todefconfig. Used to maintainarch/*/configs/files.
The .config file is architecture-specific — a config for x86 cannot be used for ARM. Always specify ARCH= when cross-compiling.
# Generate a default config for x86_64
make defconfig
# Interactively tweak the config
make menuconfig
# Build the kernel
make -j$(nproc)
# Cross-compile for ARM64:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
07The Kernel Build Pipeline: From Source to vmlinux to bzImage
When you type make, the kernel build performs a multi-stage pipeline. Understanding each stage demystifies 'where does the kernel image come from?'
Stage 1: Configuration
The .config file is read. include/generated/autoconf.h is regenerated if .config changed. Any source file that includes <linux/autoconf.h> (indirectly, via <linux/config.h>) now sees the correct CONFIG_* definitions.
Stage 2: Compilation
Every obj-y and obj-m in every Kbuild Makefile is compiled. This produces .o files. Header dependencies from .d files are respected. The C compiler is always invoked with the flags from Makefile top-level, arch/<arch>/Makefile, and whatever subsystem-specific ccflags-y variables are set.
Stage 3: Linking built-ins
In each directory, all obj-y objects are linked into a built-in.a archive. These are then linked together up the directory hierarchy. At the top level, all built-in.a archives are combined into vmlinux — the fully linked, uncompressed ELF kernel binary. This is the 'true' kernel.
Stage 4: Generating the boot image
vmlinux is then processed by architecture-specific tools:
- On x86:
objcopystrips the ELF headers to producearch/x86/boot/compressed/vmlinux.bin, which is then compressed and wrapped with a small decompressor stub to producearch/x86/boot/bzImage. The 'bz' stands for 'big zImage', not bzip2; modern kernels use gzip, lz4, or zstd. - On ARM64:
arch/arm64/boot/Imageis the uncompressed raw binary;Image.gzis the compressed form. There is no self-decompressing stub needed on ARM64 because the bootloader (U-Boot, UEFI) handles decompression.
Stage 5: Modules
obj-m objects are linked into .ko (kernel object) files. make modules_install copies them to /lib/modules/$(uname -r)/.
# After 'make', the key output files on x86:
#
# vmlinux -- ELF, fully linked, uncompressed, has debug symbols
# (~50-500 MB)
# arch/x86/boot/bzImage -- the actual bootable image (~5-15 MB compressed)
# this is what GRUB loads
# drivers/usb/core/usbcore.ko -- a loadable module
# Inspect the ELF structure of vmlinux:
readelf -h vmlinux
# See the sections:
readelf -S vmlinux | head -40
# Check the size breakdown of the kernel image:
nm --size-sort vmlinux | tail -20
08Out-of-Tree Kernel Module Builds
You do not always want to patch the kernel source tree directly. When writing a new driver or experimenting, you can build a loadable kernel module (.ko) entirely outside the kernel source tree. This is called an out-of-tree or external module build.
The mechanism: Kbuild provides a special 'external module' mode. When you run make -C /lib/modules/$(uname -r)/build M=$(pwd) modules, you are telling Make to:
- Change directory (
-C) to the kernel build directory (which contains the Kbuild infrastructure and the.configfor the running kernel). - Use the current directory (
M=$(pwd)) as the external module source tree. - Build only the
modulestarget for that external tree.
Your out-of-tree module needs only:
- The source files (e.g.
hello.c). - A minimal
Makefilewithobj-m := hello.o. - The kernel headers/build directory for the running kernel (usually installed by your distro as
linux-headers-$(uname -r)).
This is how third-party drivers like NVIDIA's proprietary driver, VirtualBox's vboxdrv, and countless others work. It is also the standard way to develop and test kernel code without rebuilding the entire kernel.
/* hello.c — minimal out-of-tree kernel module */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Minimal hello-world kernel module");
static int __init hello_init(void)
{
pr_info("hello: module loaded\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
09Out-of-Tree Module Makefile and Build Invocation
The Makefile for an out-of-tree module uses a two-phase trick. When you run make from your module directory, the Makefile first checks if KERNELRELEASE is defined. It is not defined in the first invocation (you are not inside a Kbuild context). So it re-invokes make with -C $(KDIR) to enter the Kbuild context, passing M=$(shell pwd) to point back at your source.
In the second invocation, Make runs inside the kernel build directory. Kbuild sets KERNELRELEASE, reads your directory's Makefile again, sees obj-m, and compiles your module with the correct compiler flags, include paths, and symbol tables.
This two-phase dance is the standard pattern for all out-of-tree modules. Notice that the obj-m line is the same syntax as in-tree — from Kbuild's perspective, your module looks identical to any other module.
After building, insmod hello.ko inserts the module, dmesg | tail shows your message, and rmmod hello removes it. modinfo hello.ko shows the metadata from the MODULE_* macros.
# Makefile for an out-of-tree module
# Place this in the same directory as hello.c
# If KERNELRELEASE is defined, Kbuild called us.
# Otherwise, this is the top-level invocation.
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
.PHONY: all clean
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# Build and test:
# $ make
# $ sudo insmod hello.ko
# $ dmesg | tail -3
# $ sudo rmmod hello
# $ dmesg | tail -3
10ARCH and CROSS_COMPILE: Building for a Different CPU
The Linux kernel runs on dozens of CPU architectures: x86-64, ARM (32-bit and 64-bit), RISC-V, PowerPC, MIPS, s390, and more. When you build a kernel for a CPU different from the one you are compiling on, that is cross-compilation.
Kbuild handles this through two Make variables:
ARCH: The target architecture. Valid values are the directory names underarch/:x86,arm,arm64,riscv,powerpc, etc. Note thatarm64is the Kbuild name; some external tools call itaarch64.CROSS_COMPILE: The prefix of the cross-compiler toolchain. For example,aarch64-linux-gnu-means your compiler isaarch64-linux-gnu-gcc, your linker isaarch64-linux-gnu-ld, etc.
These can be set on the make command line or exported as environment variables. The arch/<ARCH>/Makefile then sets architecture-specific flags, selects the right linker script, and specifies which image format to produce.
Cross-compiler toolchains for Linux are available from your distribution (apt install gcc-aarch64-linux-gnu) or from Linaro's releases, which provide prebuilt toolchains for ARM/ARM64. The kernel's scripts/ directory also contains helpers.
A key insight: cross-compiling the kernel is much simpler than cross-compiling userspace, because the kernel has no external library dependencies — it carries all its own infrastructure.
# Cross-compile for 64-bit ARM (Raspberry Pi 4, etc.)
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make defconfig # uses arch/arm64/configs/defconfig
make menuconfig # ARM64-specific options visible
make -j$(nproc) # produces arch/arm64/boot/Image
# Cross-compile for 32-bit RISC-V
export ARCH=riscv
export CROSS_COMPILE=riscv32-unknown-linux-gnu-
make defconfig
make -j$(nproc)
11Useful Make Targets and Debugging the Build
When you are working with the kernel build system, knowing the right make targets and debugging techniques saves enormous time.
Common targets:
make help— lists all available targets. Always start here.make V=1— verbose mode: prints every compiler and linker command as it runs. Invaluable for debugging compiler flags or understanding what Kbuild is actually doing.make C=1— run sparse (a kernel static analysis tool) on every recompiled file.make W=1— enable extra compiler warnings.make dtbs— build device tree blobs (ARM/ARM64 embedded targets).make modules— build only the modules.make modules_install INSTALL_MOD_PATH=/tmp/staging— install modules to a staging directory rather than/.make mrproper— deep clean: removes.config, generated files, everything. Equivalent togit clean -fdxon the build outputs.make clean— removes most generated files but keeps.config.
Debugging tips:
make -n(dry-run) prints what would be executed without actually doing it.make -d(debug) prints every decision Make makes about which targets need rebuilding — very verbose, but shows exactly why something is or is not being rebuilt.- The kernel's
scripts/kconfig/directory contains the C source code for all the config frontends if you want to understand Kconfig at a deeper level. grep -r 'CONFIG_FOO' arch/ drivers/is how you find where a given config symbol is used.
# See all targets
make help
# Verbose build — see every compiler invocation:
make V=1 2>&1 | head -50
# Build only a single file (useful for fast iteration):
make drivers/usb/core/usb.o
# Build with extra warnings:
make W=1 -j$(nproc)
# See what flags a specific file is compiled with:
make V=1 drivers/usb/core/usb.o 2>&1 | grep 'gcc.*usb.c'
Key insights
- ✦Make's core idea — compare timestamps and only rebuild what changed — is 50 years old and still the engine under the world's largest software project.
- ✦Kbuild is not a new build system; it is a disciplined, convention-based layer on top of GNU Make. Understanding Make first makes Kbuild obvious.
- ✦The `obj-$(CONFIG_FOO) += foo.o` pattern is the hinge that connects Kconfig (what the user wants) to Kbuild (what gets compiled). One line in a Makefile connects the user's menu choice all the way to a binary being included or excluded from the kernel image.
- ✦`.config` is just a text file. `autoconf.h` is just a generated header full of `#define`s. All the 'magic' of kernel configuration is remarkably concrete.
- ✦Cross-compilation is first-class in Kbuild: two environment variables (`ARCH` and `CROSS_COMPILE`) are all it takes to retarget a 30-million-line build for a completely different CPU.
- ✦`vmlinux` is the real kernel. `bzImage` / `Image` are packaging formats — compressed, bootloader-friendly wrappers around `vmlinux`. Understanding this distinction explains why `vmlinux` has debug symbols but the booted kernel appears smaller.
- ✦Out-of-tree modules use the exact same Kbuild machinery as in-tree modules — the two-phase Makefile trick makes your module directory look like a subtree of the kernel source from Kbuild's perspective.
- ✦Running `make help` before anything else is a habit that will save you hours of guessing.
Common pitfalls
- ⚠Indenting Makefile recipes with spaces instead of TABs. Make requires a literal TAB character. Many editors invisibly convert tabs to spaces. If you see 'missing separator' errors, this is almost always the cause.
- ⚠Using a `.config` built for one architecture when building for another. The config is architecture-specific. Always run `make ARCH=<target> defconfig` (or a board-specific defconfig) when starting a cross-build.
- ⚠Forgetting `ARCH=` and `CROSS_COMPILE=` on every `make` invocation when cross-compiling. If you set them as environment variables once and then open a new shell, they are gone.
- ⚠Running `make mrproper` when you meant `make clean`. `mrproper` deletes your `.config` too. Back it up first, or use `make clean` for most day-to-day cleaning.
- ⚠Modifying `.config` by hand and not running `make oldconfig` or `make syncconfig` afterward. Kconfig options have dependencies; hand-editing can leave the config in an inconsistent state that causes cryptic build errors.
- ⚠Building an out-of-tree module against the wrong kernel version. The module's `.ko` file contains a `vermagic` string that must exactly match the running kernel's version string, or `insmod` will refuse to load it.
- ⚠Not installing kernel headers (`linux-headers-$(uname -r)`) before trying to build an out-of-tree module on a distribution kernel. The build directory pointed to by `/lib/modules/$(uname -r)/build` will not exist.
Practice
- 01easy
Write a Makefile from scratch for a small multi-file C project (at least 3 `.c` files and 2 `.h` files). The Makefile must use: pattern rules for compilation, automatic dependency generation with `-MMD -MP`, a `clean` target, and at least one variable defined with `:=`. Verify that changing a single header file triggers recompilation of only the files that include it (use `touch` to update a header's timestamp and then run `make` again).
- 02medium
Clone the Linux kernel source (`git clone --depth=1 https://github.com/torvalds/linux`). Run `make tinyconfig` to get the smallest possible config. Then incrementally enable exactly what is needed to support a serial console on x86 (`make menuconfig`). Build with `make -j$(nproc)` and note the final `bzImage` size. Compare it to the `bzImage` size after running `make defconfig` instead. Write down what the size difference is and which three `CONFIG_*` options you had to enable for serial console support.
- 03medium
Write a complete out-of-tree kernel module that creates a read-only file at `/proc/hello` which returns the string `Hello from kernel module!\n` when read. The module must be buildable against the running kernel using only an external Makefile. Test it: build, insert, `cat /proc/hello`, remove, verify the `/proc` entry disappears.
- 04hard
Cross-compile the Linux kernel for ARM64 on an x86-64 machine. Install `gcc-aarch64-linux-gnu`. Use `ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-`. Start with `make defconfig`, then use `make menuconfig` to disable loadable module support (`CONFIG_MODULES`) and enable a fixed amount of physical memory support. Build and produce `arch/arm64/boot/Image`. Use `file arch/arm64/boot/Image` to confirm it is an ARM64 binary, not x86.
Resources
The authoritative reference for GNU Make. The chapters on rules, variables, and automatic dependency generation are essential reading.
The official kernel documentation for Kbuild, Kconfig, and module building. Start with kbuild.rst and modules.rst.
Complete reference for the Kconfig DSL — every keyword, every option type, with examples.
Greg Kroah-Hartman's free book covers kernel configuration and building in depth, written by one of the kernel's core maintainers.
Bootlin's slides cover cross-compilation, defconfig, and the full kernel build workflow for embedded targets in exhaustive, practical detail.
KernelNewbies has practical walkthroughs for first-time kernel builders and module writers.
Chapters 2 and 11 cover module building and kernel configuration in depth. Free PDF available via LWN.
A classic LWN article explaining Kbuild internals. Older but the fundamentals have not changed.
Checkpoints — you can now…
- ✓You can write a correct multi-target Makefile from scratch, with pattern rules and automatic header dependency tracking, without consulting documentation.
- ✓You can clone the Linux kernel source, run make defconfig and make menuconfig, and successfully build a bootable bzImage on x86-64.
- ✓You can explain what obj-y, obj-m, and obj-$(CONFIG_FOO) mean in a Kbuild Makefile, and what happens at compile time for each.
- ✓You can write a minimal Kconfig entry with a tristate option, a dependency, and a help string, and explain how it ultimately becomes a #define in autoconf.h.
- ✓You can build an out-of-tree kernel module from scratch — source, Makefile, build, insmod, rmmod — and explain the two-phase Makefile trick.
- ✓You can cross-compile the kernel for a different architecture by correctly setting ARCH and CROSS_COMPILE, and verify the output binary targets the right architecture.
- ✓You can use make V=1, make help, and make clean vs make mrproper appropriately, and diagnose a build failure by reading the verbose output.