Linux Kernel Compilation Essentials: Build System and Workflow
Compiling a custom Linux kernel requires more than invoking make. The kernel build system combines architecture-specific rules, Kbuild metadata, generated configuration, compiler tooling, linker scripts, and subsystem-level Makefiles into a coordinated build pipeline.
Understanding these components makes kernel compilation easier to troubleshoot and provides a clearer view of how source files become vmlinux, bzImage, loadable modules, and other runtime artifacts.
A typical workflow consists of five stages:
Kernel Source
|
v
Configuration (.config)
|
v
Kbuild / Makefiles
|
v
Compilation + Linking
|
+----> vmlinux
+----> bzImage
+----> *.ko
+----> System.map
π§© Core Linux Kernel Build Files #
The Linux kernel build system is hierarchical. The top-level Makefile establishes the build entry point, while architecture-specific and subsystem-specific Kbuild files determine what gets compiled and how individual components are assembled.
| File / Path | Function and Role |
|---|---|
Makefile |
Top-level build entry point that coordinates configuration, compilation, linking, installation, and other build targets. |
.config |
Generated kernel configuration containing CONFIG_* options that determine which kernel features and components are enabled. |
arch/$(ARCH)/Makefile |
Architecture-specific build rules, compiler settings, linker configuration, and target-specific behavior. |
scripts/Makefile.* |
Shared build infrastructure used by different parts of the kernel source tree. |
| Kbuild Makefiles | Subdirectory-level build descriptions specifying which objects, built-in components, and modules should be compiled. |
The Top-Level Makefile #
The root Makefile is the primary entry point for kernel builds.
It does not directly contain every compilation rule. Instead, it coordinates the kernel’s recursive build structure and delegates architecture- and subsystem-specific work to Kbuild.
A command such as:
make
therefore initiates a much larger dependency graph than a conventional application Makefile might suggest.
.config
#
The .config file controls the feature set of the resulting kernel.
Configuration options follow the familiar:
CONFIG_<OPTION>=<VALUE>
format.
For example:
CONFIG_SMP=y
CONFIG_MODULES=y
CONFIG_NET=y
The configuration influences whether components are compiled directly into the kernel, built as loadable modules, or excluded entirely.
Because .config directly affects the build graph, configuration problems can manifest as missing symbols, unavailable drivers, unexpected dependencies, or linker failures.
Architecture-Specific Makefiles #
Architecture-specific build logic resides under:
arch/$(ARCH)/
For example:
arch/x86/Makefile
arch/arm64/Makefile
arch/riscv/Makefile
These files define architecture-dependent compiler and linker behavior and integrate architecture-specific source code into the generic kernel build system.
This separation allows the majority of kernel subsystems to remain architecture-independent while CPU-specific initialization, exception handling, memory management, and low-level interfaces remain isolated.
Kbuild Makefiles #
Kbuild is the kernel’s internal build infrastructure.
Subsystem directories contain Makefiles describing objects and modules to build. For example, a simplified Kbuild declaration can look like:
obj-y += example.o
obj-m += example_driver.o
The distinction is significant:
obj-ycontributes code to the built-in kernel.obj-mbuilds a loadable kernel module.
Kbuild then resolves dependencies and invokes the appropriate compiler and linker commands.
βοΈ Essential Kernel make Targets
#
Several make targets are particularly important when compiling a custom kernel.
make mrproper
#
Performs a deep cleanup of generated build state:
make mrproper
Unlike a normal clean build, mrproper can remove the active .config file along with generated files and build artifacts.
A configuration that needs to be preserved should therefore be backed up before running this command.
make clean
#
Removes generated build artifacts while preserving the current kernel configuration:
make clean
This is useful when you want to force recompilation without starting configuration from scratch.
make menuconfig
#
Launches the ncurses-based kernel configuration interface:
make menuconfig
It allows developers to enable or disable kernel features, drivers, filesystems, networking functionality, debugging options, and other configuration items.
The resulting configuration is stored in:
.config
For automated or reproducible builds, configuration can also be supplied from a known baseline rather than manually recreated through menuconfig.
make bzImage
#
On x86, the bzImage target produces a compressed bootable kernel image:
make bzImage
For parallel compilation:
make bzImage -j$(nproc)
Despite the name, bzImage does not mean the kernel is necessarily compressed using bzip2. The name originates from the historical x86 boot-image format and distinguishes it from the older zImage format.
The resulting image is typically found under an architecture-dependent path such as:
arch/x86/boot/bzImage
make modules
#
Builds kernel components configured as loadable modules:
make modules
Parallel compilation can be enabled with:
make modules -j$(nproc)
Successful module builds produce .ko files.
make modules_install
#
Installs compiled modules into the appropriate module directory:
sudo make modules_install
The destination normally resembles:
/lib/modules/<kernel-release>/
The exact contents include module binaries, dependency metadata, symbol information, and other files required by the module-loading infrastructure.
make install
#
The generic kernel installation target can install the kernel image and associated metadata:
sudo make install
On many distributions, kernel installation also interacts with bootloader and initramfs tooling. However, the exact behavior depends on the distribution, kernel version, and installed integration scripts.
π From Object Files to the Kernel Image #
The kernel build process transforms thousands of source files into a relatively small set of final artifacts.
Conceptually:
.c / .S source files
|
v
compiler / assembler
|
v
*.o
|
v
built-in.a / modules
|
v
vmlinux
|
+--------------------+
| |
v v
bootable image debug analysis
bzImage
During the link stage, the kernel build infrastructure collects built-in objects and libraries and ultimately produces the main kernel executable.
Important outputs include vmlinux, bzImage, loadable .ko modules, and System.map.
π§ Understanding vmlinux
#
vmlinux is the uncompressed kernel image produced by the build system.
It is an ELF executable containing the kernel’s linked code and, depending on the build configuration and symbol handling, debugging information.
This makes vmlinux particularly useful for:
- GDB kernel debugging
- Symbol resolution
- Static analysis
- Crash investigation
- Examining kernel sections
- Mapping addresses back to functions
The file is not normally the image directly loaded by the x86 bootloader. Instead, the architecture-specific boot image is generated from the kernel build outputs.
For debugging, vmlinux is often more valuable than the compressed boot image because it retains the symbol information required by debugging tools.
πΊοΈ Understanding System.map
#
System.map contains a mapping between kernel symbol addresses and symbol names.
A simplified entry looks conceptually like:
ffffffff81000000 T _stext
ffffffff81000120 T start_kernel
ffffffff81000280 T schedule
This mapping can help translate raw addresses from kernel diagnostics into human-readable symbol names.
System.map is therefore useful when investigating:
- Kernel oops messages
- Stack traces
- Symbol addresses
- Low-level debugging problems
It is important to distinguish System.map from the kernel’s runtime symbol infrastructure. System.map itself is not loaded into kernel memory as an executable component during normal operation.
ποΈ Practical Linux Kernel Compilation Workflow #
The following example demonstrates a basic custom-kernel build workflow on an Ubuntu-based system.
The source tree is assumed to contain a Linux 6.1.x kernel:
# 1. Extract the kernel source
tar -xf linux-6.1.1.tar.xz
cd linux-6.1.1
# 2. Configure the kernel
make menuconfig
# 3. Build the kernel image
make bzImage -j$(nproc)
# 4. Build loadable modules
make modules -j$(nproc)
# 5. Install modules
sudo make INSTALL_MOD_STRIP=1 modules_install
# 6. Install the kernel
sudo make install
# 7. Reboot into the new kernel
sudo reboot
# 8. Verify the running kernel
uname -r
Preserving the Existing Kernel Configuration #
When building a kernel for an existing machine, starting from the platform’s current configuration is often more practical than constructing a configuration from scratch.
A common approach is:
cp /boot/config-$(uname -r) .config
make olddefconfig
olddefconfig automatically accepts defaults for configuration options that are new relative to the supplied configuration.
This can provide a more stable baseline when experimenting with a newer kernel source tree.
Parallel Compilation #
Kernel builds are highly parallelizable.
Using:
make -j$(nproc)
allows Kbuild to schedule multiple independent compilation tasks concurrently.
The optimal job count is workload-dependent. $(nproc) provides a useful starting point, but memory capacity and compiler workload can make a lower value more efficient on constrained systems.
π§ͺ Module Installation and Debug Symbols #
The example uses:
sudo make INSTALL_MOD_STRIP=1 modules_install
INSTALL_MOD_STRIP=1 strips debugging information from installed module binaries, reducing disk usage.
This can be useful on production systems where module debugging symbols are not required.
For kernel development, however, retaining debugging information can be preferable because it improves post-mortem analysis and interactive debugging.
A useful development strategy is therefore to distinguish between:
- Development builds: retain symbols and debugging information.
- Production builds: strip unnecessary symbols and debugging metadata.
The decision should be based on the debugging and operational requirements of the target system.
π Troubleshooting the Kernel Build #
Kernel compilation failures are often caused by configuration mismatches, missing dependencies, stale build state, architecture-specific assumptions, or unresolved symbols.
A useful troubleshooting sequence is:
Build Failure
|
v
Read the First Relevant Error
|
v
Check .config
|
v
Check Toolchain / Dependencies
|
v
Check Architecture Configuration
|
v
Clean or Reconfigure
|
v
Rebuild with V=1
Inspecting the Actual Compiler Commands #
When a build fails and the normal output does not provide enough information, use:
make V=1
This exposes the underlying compiler and linker invocations.
It can reveal problems involving:
- Incorrect include paths
- Compiler flags
- Linker arguments
- Architecture selection
- Missing generated headers
- Unexpected toolchain versions
Avoiding Unnecessary Full Cleans #
A full:
make mrproper
should not be the first response to every compilation failure.
Because it removes .config, it can also destroy useful configuration state.
A more targeted approach is usually preferable:
make clean
or regenerate configuration state with:
make olddefconfig
Only use a deeper cleanup when stale generated state is genuinely suspected.
π¦ Important Build Artifacts #
A custom kernel build can generate many files, but several outputs are particularly important:
| Artifact | Purpose |
|---|---|
.config |
Records enabled kernel configuration options. |
vmlinux |
Uncompressed linked ELF kernel image, useful for debugging and analysis. |
arch/x86/boot/bzImage |
Compressed bootable x86 kernel image. |
System.map |
Kernel symbol-address mapping useful for diagnostics and debugging. |
*.ko |
Loadable kernel modules. |
Module.symvers |
Symbol-version information used for module builds and symbol compatibility. |
modules.order |
Records the module build order used by Kbuild. |
The exact set of generated files varies with kernel version, architecture, configuration, and build options.
π Kernel Build System Mental Model #
The most useful way to understand Linux kernel compilation is to treat Kbuild as a dependency-driven build graph rather than a single Makefile.
+----------------+
| .config |
+--------+-------+
|
v
+-------------+ +-------------------+
| Top-level |------>| Kbuild |
| Makefile | +---------+---------+
+-------------+ |
|
+----------------+----------------+
| | |
v v v
arch/$(ARCH) drivers/ fs/ ...
Makefiles Kbuilds Kbuilds
| | |
+----------------+----------------+
|
v
Object Compilation
|
v
built-in.a / *.o / *.ko
|
v
Link Stage
|
+-----------+-----------+
| |
v v
vmlinux Loadable Modules
|
v
bzImage
This model explains why a change to .config, architecture selection, or a subsystem Kbuild file can affect seemingly unrelated parts of the final build.
π Conclusion #
Linux kernel compilation is a layered build process driven by Kbuild, configuration metadata, architecture-specific rules, and subsystem-level Makefiles.
The most important concepts are straightforward once their roles are separated:
.configdetermines what the kernel contains.- Kbuild determines how configured components are compiled.
vmlinuxis the primary linked kernel executable and an important debugging artifact.bzImageis the compressed bootable x86 kernel image.*.kofiles contain loadable kernel modules.System.mapprovides a useful symbol-to-address mapping for diagnostics.make menuconfig,make,make modules, andmake modules_installform the core development workflow.
With this model in place, kernel build errors become easier to reason about because each stageβfrom configuration through compilation and linking to installationβhas a defined responsibility and a distinct set of artifacts.