Skip to main content

Linux Kernel Compilation Essentials: Build System and Workflow

·1884 words·9 mins
Linux Linux Kernel Kernel Compilation Kbuild Kernel Development Build System Kernel Debugging Ubuntu
Table of Contents

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-y contributes code to the built-in kernel.
  • obj-m builds 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:

  • .config determines what the kernel contains.
  • Kbuild determines how configured components are compiled.
  • vmlinux is the primary linked kernel executable and an important debugging artifact.
  • bzImage is the compressed bootable x86 kernel image.
  • *.ko files contain loadable kernel modules.
  • System.map provides a useful symbol-to-address mapping for diagnostics.
  • make menuconfig, make, make modules, and make modules_install form 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.

Related

Linux 7.2 Removes strncpy After 37 Years of C Safety Debt
·1405 words·7 mins
Linux C Programming Rust Kernel Development Memory Safety Systems Programming Open Source Software Security Linux Kernel
Understanding the Linux Kernel (3rd Edition): Complete Guide
·1033 words·5 mins
Linux Linux Kernel Kernel Development Operating-Systems System Programming VFS Memory Management Kernel Architecture
Linux Kernel Development (3rd Edition): Complete Technical Overview
·1141 words·6 mins
Linux Linux Kernel Operating-Systems Kernel Development Systems Programming Linux Internals Computer Science Software-Engineering