Loading content...
Compiling the Linux kernel transforms millions of lines of source code into a single bootable image and thousands of loadable modules. This process involves:
Understanding kernel compilation is essential for custom kernel development, embedded systems work, debugging build issues, and optimizing build times. This page takes you through every step of the process.
By the end of this page, you will understand: (1) Prerequisites and build environment setup, (2) The kbuild system architecture, (3) Build targets and their purposes, (4) Parallel and incremental building, (5) Cross-compilation workflows, (6) Installation procedures, and (7) Build troubleshooting techniques.
Before compiling the kernel, you need the proper toolchain and dependencies installed. Requirements vary slightly by distribution.
Essential Build Tools:
| Tool | Purpose | Minimum Version |
|---|---|---|
gcc | C compiler | 5.1+ (11+ recommended) |
make | Build automation | 4.0+ |
binutils | Assembler, linker (as, ld) | 2.25+ |
flex | Lexical analyzer generator | 2.5.35+ |
bison | Parser generator | 2.0+ |
bc | Calculator (for kernel math) | Any |
libelf-dev | ELF handling library | Any (for BTF, objtool) |
libssl-dev | Cryptographic functions | Any (for module signing) |
perl | Scripting (build scripts) | 5.x |
cpio | Archive tool (initramfs) | Any |
xz-utils | Compression | Any |
12345678910111213141516171819202122232425262728
# Debian/Ubuntu - Install all build dependencies$ sudo apt-get install build-essential libncurses-dev \ bison flex libssl-dev libelf-dev bc dwarves \ zstd git fakeroot dpkg-dev # Also for full builds with BTF (BPF Type Format)$ sudo apt-get install pahole # DWARF to BTF converter # Fedora/RHEL/CentOS$ sudo dnf install gcc make ncurses-devel bison flex \ elfutils-libelf-devel openssl-devel bc perl \ dwarves zstd git # Arch Linux$ sudo pacman -S base-devel bc libelf pahole perl \ xmlto kmod inetutils # Verify GCC version (should be 11+ for modern kernels)$ gcc --versiongcc (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0 # Check required minimum versions$ make --version | head -1GNU Make 4.3$ flex --versionflex 2.6.4$ bison --version | head -1bison (GNU Bison) 3.8.2Disk Space Requirements:
| Build Type | Approximate Space Required |
|---|---|
| Source tree only | ~1.5 GB |
| Full build (allmodconfig) | ~20-30 GB |
| Typical build | ~5-10 GB |
| Minimal build | ~2-3 GB |
| Git history included | +4 GB |
Memory Requirements:
Build Time Estimates (on modern 8-core system):
| Build Type | Time |
|---|---|
| Full allmodconfig | 60-120 minutes |
| Distribution config | 30-60 minutes |
| localmodconfig | 10-20 minutes |
| Incremental (small change) | ~1 minute |
For kernel development involving frequent rebuilds, ccache dramatically speeds up compilation by caching object files: $ sudo apt install ccache then $ export CC="ccache gcc" before building. Subsequent builds of unchanged files are nearly instant.
Linux uses a custom build system called Kbuild, built on top of GNU Make. Kbuild handles the unique challenges of kernel compilation:
Makefile Hierarchy:
linux/├── Makefile # Top-level Makefile (entry point)├── scripts/│ ├── Makefile.* # Build infrastructure│ ├── Kbuild.include # Common make rules│ ├── Makefile.build # Recursive build logic│ ├── Makefile.lib # Library building rules│ └── Makefile.modinst # Module installation│├── arch/x86/│ ├── Makefile # Architecture-specific rules│ └── boot/│ └── Makefile # Boot image building│├── drivers/│ ├── Makefile # Drivers top-level│ └── usb/│ └── Makefile # USB drivers│└── (every directory has a Makefile or Kbuild file)How Kbuild Works:
Kbuild uses a simple pattern in each directory's Makefile:
# drivers/usb/Makefile
obj-$(CONFIG_USB_SUPPORT) += core/
obj-$(CONFIG_USB_XHCI_HCD) += host/
# drivers/usb/core/Makefile
obj-$(CONFIG_USB) += usbcore.o
usbcore-y := usb.o hub.o hcd.o
usbcore-$(CONFIG_PCI) += hcd-pci.o
Pattern Breakdown:
| Pattern | Meaning |
|---|---|
obj-$(CONFIG_X) | Include if CONFIG_X is y or m |
obj-y | Always include (built-in) |
obj-m | Always build as module |
foo-y | Object files comprising foo target |
foo-objs | Same as foo-y |
subdir-y | Subdirectories to descend into |
ccflags-y | Compiler flags for this directory |
asflags-y | Assembler flags for this directory |
The Build Flow:
make reads Makefile, includes .config.c files to .o object filesvmlinux (uncompressed kernel)bzImage, Image).ko filesvmlinux: The raw, uncompressed kernel ELF binary with debug symbols (~500MB+ with debug info). vmlinuz: Compressed kernel, the 'z' indicates compression. bzImage: 'Big zImage'—the bootable x86 image with decompressor stub. For x86, bzImage is what you install to /boot.
The kernel Makefile provides numerous targets for different build operations:
| Target | Purpose | Output |
|---|---|---|
make or make all | Build kernel + modules | vmlinux, bzImage, *.ko |
make vmlinux | Build uncompressed kernel only | vmlinux (ELF) |
make bzImage | Build compressed x86 kernel | arch/x86/boot/bzImage |
make modules | Build loadable modules only | *.ko throughout tree |
make clean | Remove most generated files | Keeps .config |
make mrproper | Remove all generated files | Removes .config too |
make distclean | mrproper + editor backups | Pristine source |
| Target | Purpose | Destination |
|---|---|---|
make install | Install kernel image | /boot/vmlinuz-VERSION |
make modules_install | Install modules | /lib/modules/VERSION/ |
make headers_install | Install userspace headers | INSTALL_HDR_PATH |
make firmware_install | Install firmware (deprecated) | /lib/firmware/ |
12345678910111213141516171819202122232425262728293031323334
# Standard kernel build workflow # 1. Configure (see previous page)$ make menuconfig # 2. Build kernel and modules (parallel with all cores)$ make -j$(nproc)# -j$(nproc) = number of parallel jobs = number of CPU cores# Output: vmlinux, arch/x86/boot/bzImage, *.ko files # Check build progress (if running in background)$ make -j$(nproc) 2>&1 | tee build.log # 3. Install modules (requires root for /lib/modules)$ sudo make modules_install# Creates /lib/modules/$(make kernelrelease)/# Runs depmod to generate module dependencies # 4. Install kernel image (requires root for /boot)$ sudo make install# Copies bzImage to /boot/vmlinuz-VERSION# Copies System.map to /boot/System.map-VERSION# May run bootloader update (distribution-specific) # 5. Generate initramfs (distribution-specific)# Debian/Ubuntu:$ sudo update-initramfs -c -k $(make kernelrelease)# Fedora:$ sudo dracut --force /boot/initramfs-$(make kernelrelease).img \ $(make kernelrelease) # 6. Update bootloader (if not done by make install)$ sudo update-grub # Debian/Ubuntu$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg # FedoraBy default, Kbuild shows brief output (CC drivers/usb/core/hub.o). For full command lines, use make V=1. For even more detail (including why rebuilds happen), use make V=2. Essential for debugging build issues.
Kernel compilation benefits enormously from parallelism. The build system automatically tracks dependencies, enabling safe parallel compilation.
Parallel Building:
# Build with N parallel jobs
$ make -jN
# Common patterns:
$ make -j$(nproc) # Use all CPU cores
$ make -j$(nproc --all) # Use all cores including hyperthreads
$ make -j8 # Fixed 8 parallel jobs
$ make -j # Unlimited parallelism (risky)
# Optimal value depends on:
# - CPU cores (more = faster)
# - Memory (each job uses memory, limit if swapping)
# - Disk I/O (diminishing returns beyond CPU count)
# Rule of thumb: -j$(nproc) for most systems
# For memory-constrained: -j$(($(nproc) / 2))
Build Time Comparison (8-core system, distribution config):
| Parallelism | Time |
|---|---|
make (j=1) | ~180 minutes |
make -j4 | ~50 minutes |
make -j8 | ~30 minutes |
make -j16 | ~28 minutes (diminishing returns) |
Incremental Builds:
Kbuild tracks dependencies automatically. After an initial build, only changed files and their dependents rebuild:
# Initial full build
$ make -j$(nproc) # Takes 30+ minutes
# Modify a single file
$ vim kernel/sched/fair.c
# Incremental rebuild
$ make -j$(nproc) # Only rebuilds fair.o, re-links
# # Takes ~30 seconds
# After config change, more may rebuild
$ ./scripts/config --enable CONFIG_DEBUG_INFO
$ make -j$(nproc) # Rebuilds affected files
Dependency Tracking:
Kbuild generates .cmd files alongside object files containing dependency information:
$ cat kernel/sched/.fair.o.cmd
cmd_kernel/sched/fair.o := gcc -o kernel/sched/fair.o ... fair.c
deps_kernel/sched/fair.o := \
include/linux/sched.h \
include/linux/list.h \
...
These files enable accurate incremental builds. Deleting them forces full rebuild.
/tmp or fast NVMe helps.If you experience strange behavior after incremental builds, try make clean followed by a full rebuild. Rare edge cases (changing critical headers, config changes affecting many files) can confuse dependency tracking. This is uncommon but worth knowing.
Cross-compilation builds a kernel for a different CPU architecture than the host. This is essential for embedded development where target devices lack the resources for native compilation.
Key Variables:
| Variable | Purpose | Example |
|---|---|---|
ARCH | Target architecture | arm64, arm, riscv |
CROSS_COMPILE | Compiler prefix | aarch64-linux-gnu- |
INSTALL_MOD_PATH | Module install location | /mnt/target_root |
INSTALL_PATH | Kernel install location | /mnt/target_boot |
123456789101112131415161718192021222324252627282930313233343536373839
# Install cross-compilation toolchain# Debian/Ubuntu:$ sudo apt install gcc-aarch64-linux-gnu # ARM64$ sudo apt install gcc-arm-linux-gnueabihf # ARM 32-bit (hard float)$ sudo apt install gcc-riscv64-linux-gnu # RISC-V 64-bit # Fedora:$ sudo dnf install gcc-aarch64-linux-gnu # Verify toolchain$ aarch64-linux-gnu-gcc --version # Method 1: Set environment variables$ export ARCH=arm64$ export CROSS_COMPILE=aarch64-linux-gnu-$ make defconfig$ make menuconfig$ make -j$(nproc) # Method 2: Pass on command line (each make invocation)$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) # Install to custom location (for copying to target)$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \ INSTALL_MOD_PATH=/mnt/target_root modules_install$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \ INSTALL_PATH=/mnt/target_boot install # Build results for ARM64# - arch/arm64/boot/Image (uncompressed kernel)# - arch/arm64/boot/Image.gz (compressed kernel)# - *.ko modules # For Raspberry Pi (ARM 32-bit)$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2711_defconfig$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \ INSTALL_MOD_PATH=/mnt/sdcard modules_installCross-Compilation for Embedded Systems:
Embedded workflows often involve:
Device Tree Compilation: Many ARM/RISC-V boards need Device Tree Blobs (DTBs):
$ make ARCH=arm64 dtbs
# Output: arch/arm64/boot/dts/vendor/*.dtb
Minimal Configs: Start from board-specific defconfigs:
$ ls arch/arm64/configs/
defconfig bcm2711_defconfig ...
Image Formats: Different bootloaders need different formats:
uImage (make ARCH=arm uImage)Image or Image.gzbzImage with EFI stubTransferring to Target:
# Copy kernel image
$ scp arch/arm64/boot/Image root@target:/boot/
# Copy modules
$ rsync -av /mnt/target_root/lib/modules/ root@target:/lib/modules/
For reproducible cross-compilation environments, consider Docker containers with pre-installed toolchains. The kernelci project provides images: docker run -v $(pwd):/linux kernelci/build-gcc-10_arm64 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
Third-party modules (like NVIDIA drivers, VirtualBox, or custom drivers) are built against an installed kernel's headers rather than the full source tree.
Requirements for Out-of-Tree Builds:
Kernel Headers: Package providing build infrastructure
# Debian/Ubuntu
$ sudo apt install linux-headers-$(uname -r)
# Fedora
$ sudo dnf install kernel-devel
# Check installation
$ ls /lib/modules/$(uname -r)/build
Module Source: Your driver's source code
Proper Makefile: Using Kbuild conventions
1234567891011121314151617181920212223
// hello.c - Simple out-of-tree module#include <linux/init.h>#include <linux/module.h>#include <linux/kernel.h> MODULE_LICENSE("GPL");MODULE_AUTHOR("Developer");MODULE_DESCRIPTION("Hello World Module");MODULE_VERSION("1.0"); static int __init hello_init(void){ printk(KERN_INFO "Hello, kernel!\n"); return 0;} static void __exit hello_exit(void){ printk(KERN_INFO "Goodbye, kernel!\n");} module_init(hello_init);module_exit(hello_exit);123456789101112131415161718192021222324252627282930
# Makefile for out-of-tree kernel module # Module name (without .ko extension)obj-m := hello.o # For multi-file modules:# obj-m := mydriver.o# mydriver-objs := main.o helper.o # Kernel build directoryKDIR ?= /lib/modules/$(shell uname -r)/build # Current directory (module source)PWD := $(shell pwd) # Default target: build against running kernelall: $(MAKE) -C $(KDIR) M=$(PWD) modules # Clean generated filesclean: $(MAKE) -C $(KDIR) M=$(PWD) clean # Install moduleinstall: $(MAKE) -C $(KDIR) M=$(PWD) modules_install # Build against specific kernel headers# Usage: make KDIR=/path/to/kernel/build 1234567891011121314151617181920212223242526272829
# Build the module$ makemake -C /lib/modules/6.6.0/build M=/home/dev/hello modules CC [M] /home/dev/hello/hello.o MODPOST /home/dev/hello/Module.symvers CC [M] /home/dev/hello/hello.mod.o LD [M] /home/dev/hello/hello.ko # View module info$ modinfo hello.kofilename: /home/dev/hello/hello.koversion: 1.0description: Hello World Moduleauthor: Developerlicense: GPLvermagic: 6.6.0 SMP preempt mod_unload # Load the module$ sudo insmod hello.ko$ dmesg | tail -1[12345.678] Hello, kernel! # Unload the module$ sudo rmmod hello$ dmesg | tail -1[12345.789] Goodbye, kernel! # Build against different kernel version$ make KDIR=/lib/modules/6.5.0/build clean allOut-of-tree modules must be recompiled for each kernel version. The kernel has no stable ABI—internal structures change between versions. DKMS (Dynamic Kernel Module Support) automates rebuilding modules when kernels are upgraded: sudo apt install dkms.
After building, the kernel must be properly installed and the bootloader configured to offer it at boot time.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
# Get the kernel version string$ make kernelrelease6.6.0-custom # Complete installation process # Step 1: Install modules$ sudo make modules_install# - Copies *.ko to /lib/modules/6.6.0-custom/kernel/# - Runs depmod to generate modules.dep # Verify$ ls /lib/modules/6.6.0-custom/build kernel modules.alias modules.dep modules.symbols source # Step 2: Install kernel image$ sudo make install# - Copies arch/x86/boot/bzImage → /boot/vmlinuz-6.6.0-custom# - Copies System.map → /boot/System.map-6.6.0-custom # - Copies .config → /boot/config-6.6.0-custom# - May run distribution hook scripts # Verify$ ls /boot/*6.6.0-custom*/boot/config-6.6.0-custom/boot/System.map-6.6.0-custom/boot/vmlinuz-6.6.0-custom # Step 3: Create initramfs (required for most systems)# Debian/Ubuntu:$ sudo update-initramfs -c -k 6.6.0-customupdate-initramfs: Generating /boot/initrd.img-6.6.0-custom # Fedora/RHEL:$ sudo dracut --force /boot/initramfs-6.6.0-custom.img 6.6.0-custom # Arch Linux:$ sudo mkinitcpio -k 6.6.0-custom -g /boot/initramfs-6.6.0-custom.img # Step 4: Update bootloader# GRUB (most common):$ sudo update-grub# or$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg # Verify GRUB sees the new kernel$ grep menuentry /boot/grub/grub.cfg | grep 6.6.0-custommenuentry 'Ubuntu, with Linux 6.6.0-custom' ... # Step 5: Reboot and select new kernel$ sudo rebootManual Installation (without make install):
Sometimes you need fine-grained control:
# Copy kernel image manually
$ sudo cp arch/x86/boot/bzImage /boot/vmlinuz-6.6.0-custom
$ sudo cp System.map /boot/System.map-6.6.0-custom
$ sudo cp .config /boot/config-6.6.0-custom
# Set proper permissions
$ sudo chmod 644 /boot/vmlinuz-6.6.0-custom
# Create initramfs
$ sudo update-initramfs -c -k 6.6.0-custom
# Add GRUB entry manually (if needed)
$ sudo cat >> /etc/grub.d/40_custom << 'EOF'
menuentry 'Linux 6.6.0-custom' {
set root='hd0,gpt2'
linux /vmlinuz-6.6.0-custom root=/dev/nvme0n1p3 ro quiet
initrd /initrd.img-6.6.0-custom
}
EOF
$ sudo update-grub
Always keep at least one known-working kernel installed! If your new kernel fails to boot, you can select the old one from the GRUB menu. Never delete your backup kernel until you've verified the new one works correctly.
Kernel builds can fail for various reasons. Here's how to diagnose and fix common issues:
| Error | Cause | Solution |
|---|---|---|
recipe for target 'scripts/...' failed | Missing build dependency | Install flex, bison, libelf-dev, etc. |
No rule to make target '...config' | Corrupt/missing config | make mrproper then reconfigure |
undefined reference to ... | Missing dependency in config | Enable the required CONFIG option |
error: unknown type name 'bool' | Headers issue | make mrproper and rebuild |
BTF: .tmp_vmlinux.btf: No such file | Missing pahole/dwarves | Install dwarves or disable BTF |
modpost: missing MODULE_LICENSE() | Module license issue | Add MODULE_LICENSE to source |
SSL error:... | Module signing issue | Install libssl-dev or disable signing |
| Out of memory | Too many parallel jobs | Reduce -j value or add swap |
1234567891011121314151617181920212223242526272829303132333435363738
# Enable verbose output for debugging$ make V=1 -j$(nproc) 2>&1 | tee build.log # Find exact error in log$ grep -i error build.log$ grep -B5 'error:' build.log # Show 5 lines before error # Clean build when confused by incremental issues$ make mrproper # Remove ALL generated files$ make defconfig # Start fresh$ make -j$(nproc) # Missing BTF support (common on older distros)$ grep BTF .configCONFIG_DEBUG_INFO_BTF=y # This requires 'pahole'# Fix: Either install pahole or disable BTF$ ./scripts/config --disable CONFIG_DEBUG_INFO_BTF$ make olddefconfig$ make -j$(nproc) # Missing module signing key$ grep MODULE_SIG .configCONFIG_MODULE_SIG=yCONFIG_MODULE_SIG_KEY="certs/signing_key.pem"# If key doesn't exist and you don't need signing:$ ./scripts/config --disable CONFIG_MODULE_SIG$ make olddefconfig # Check for missing headers/tools$ make -j$(nproc) 2>&1 | grep -i "command not found"$ make -j$(nproc) 2>&1 | grep -i "no such file" # Build single file to isolate errors$ make drivers/usb/core/hub.o V=1 # Verify configuration consistency$ make olddefconfig# This reports if anything was auto-fixedIf you're building from a git checkout and having strange issues, git clean -fdx removes ALL untracked files (including .config!). Save your config first: cp .config ../saved.config, then git clean -fdx, then restore. This ensures truly pristine source.
-j$(nproc) dramatically reduces build timeWhat's Next:
With configuration and compilation mastered, we conclude with Kernel Versioning—understanding version numbers, release cycles, LTS vs stable, and choosing the right kernel for your needs.
You now have complete knowledge of kernel compilation, from environment setup through installation. This capability enables custom kernel development, embedded systems work, performance optimization, and debugging at the deepest level.