L26: Running the Kernel Build

 

Building the Linux 6.x Kernel from Source

Lecture 26  |  Step 4: Compile the Kernel Image and Modules  |  Free Linux Kernel Development Course

🐧 Linux 6.x
⚙ Step 4 of 7
👍 Free Course
🎯 Beginner Friendly

What You Will Learn

make bzImage
vmlinux vs bzImage
Parallel Build -j flag
Kernel Modules .ko
kbuild system
nproc command
System.map
zstd module compression
make clean vs mrproper

Where Are We in the Build Journey?

In the previous lecture you downloaded the kernel source and ran menuconfig to produce a .config file. That configuration tells the build system exactly what to compile. Now in Step 4 we actually run the compiler and produce the kernel binary and all its loadable modules.

This lecture covers what make builds, the difference between vmlinux and bzImage, how to speed up the build with parallel jobs, prerequisites specific to Linux 6.x, and what to do when things go wrong.

Running the Kernel Build

Once your .config exists at the root of the kernel source tree, building is a single command. The kernel build system is called kbuild and it handles all the complexity behind the scenes.

cd /path/to/linux-6.x
make -j$(nproc)

Running make without a target is identical to make all. The kbuild system checks which files changed since the last build and only recompiles those. The very first build takes the longest — on a 4-core machine expect 25 to 45 minutes.

💡 Tip — Explore make help

Run make help from the kernel source root to see every available target. Targets prefixed with * are the ones built automatically when you run make all. This is the best way to discover what the build system can do without reading through thousands of lines of Makefiles.

What Gets Built by Default

On an x86_64 machine, make all produces three types of output. Understanding what each one is and where it goes is fundamental to kernel development.

make all — Three Default Output Types on x86_64

① vmlinux — Uncompressed Kernel ELF Binary

The raw linked kernel binary sitting at the source root. Never booted directly. Used by debugging tools that need kernel symbols.

For debugging only Stays in source root Can be very large

② bzImage — Compressed Bootable Kernel Image

The actual kernel you boot from. GRUB loads this file into RAM. It self-extracts and starts the kernel. Lives at arch/x86/boot/bzImage.

Copied to /boot Loaded by GRUB Self-extracting

③ *.ko.zst — Loadable Kernel Modules

Each config option set to M in menuconfig becomes a separate .ko.zst file. Scattered across the source tree. Installed to /lib/modules/ in Step 5.

Installed to /lib/modules Load on demand zstd compressed in 6.x

vmlinux — The Uncompressed Kernel Binary

vmlinux is the raw ELF binary of the compiled kernel. Every compiled object file is linked into this single binary. It lives at the root of your kernel source directory after the build. You never boot from vmlinux directly — so why does it get built?

The answer is kernel debugging. The vmlinux file carries all kernel symbols — function names, variable names, and their exact memory addresses at the time of the build. Debugging tools need this to make sense of what the kernel is doing.

Who Uses vmlinux and Why

🔍 gdb

Set breakpoints inside the kernel, inspect kernel data structures while the system runs under a hypervisor or via KGDB.

🔌 crash tool

Analyse kernel core dumps (vmcore) after a kernel panic. Decodes stack traces and shows what the kernel was doing when it crashed.

📋 System.map

Generated from vmlinux. Maps every function and variable to its memory address. Used by /proc/kallsyms and crash analysis.

💡 Always Keep vmlinux and System.map

Even if you never plan to debug the kernel, store vmlinux and System.map in a safe location after your build. If you hit a kernel oops later, these two files let you decode the crash — without them the stack trace is just raw hex addresses.

bzImage — The Bootable Kernel

bzImage stands for big zImage. It is a compressed, self-extracting kernel image that the bootloader loads into RAM at boot time. Here is exactly what happens:

Boot Time Flow — How bzImage Is Used

① BIOS or UEFI powers on → hands control to GRUB

GRUB reads /boot/grub/grub.cfg and shows the kernel selection menu.

② GRUB loads /boot/bzImage and the initramfs into RAM

Both files are read from disk and placed in memory. GRUB then jumps to the kernel entry point.

③ Kernel decompresses itself in memory and starts running

The bzImage self-extractor unpacks the real kernel into RAM. Hardware initialisation begins.

④ Kernel mounts root filesystem → starts systemd → login prompt ✓

Normal system boot continues. The kernel hands control to userspace.

On x86_64 the bootable image is always found at:

arch/x86/boot/bzImage

On ARM64 (Raspberry Pi 4/5, custom SoCs) the equivalent is Image or Image.gz found under arch/arm64/boot/. On 32-bit ARM it is zImage under arch/arm/boot/.

💡 No Location Change in Linux 6.x

The bzImage name and path are unchanged across all modern kernels from 5.x through 6.12 LTS. What did change are the toolchain requirements for building it, covered in the prerequisites section below.

Kernel Modules — Loadable .ko Files

Any kernel feature set to M in menuconfig gets compiled into a separate .ko file. Modules are not part of the main kernel image. They get loaded into the running kernel on demand without rebooting. This is what makes Linux so flexible — you can load a driver for a USB device the moment you plug it in, then unload it when you remove the device.

Examples of Kernel Modules by Subsystem

🕸 igb.ko — Intel Gigabit NIC
🕸 iwlwifi.ko — Intel Wi-Fi
🕸 cfg80211.ko — 802.11 framework
💾 ext4.ko — ext4 filesystem
💾 btrfs.ko — Btrfs filesystem
🎮 i915.ko — Intel GPU
🎮 amdgpu.ko — AMD GPU
🔊 snd-hda-intel.ko — Intel HDA audio
🔌 btusb.ko — Bluetooth USB
🔌 xhci-hcd.ko — USB 3.0 host

💡 Linux 6.x Change — Modules Are Now .ko.zst

Since kernel 5.13, the build system compresses module files with zstd by default. Your compiled module files will carry the .ko.zst extension instead of plain .ko. The kernel loads them transparently — no manual decompression needed. Ensure the zstd package is installed on your build machine before running make.

Making the Build Fast — Parallel Compilation

The Linux 6.x kernel source has over 30 million lines of code. A single-threaded build on a 4-core machine takes 30 to 60 minutes. The -j flag enables parallel compilation and cuts this dramatically.

# See how many CPU cores your machine has
nproc

# Build using 2x core count (the recommended formula)
time make -j$(nproc)

The formula for the -j value:

n = number_of_CPU_cores x 2

Build Time Comparison on a 4-Core Machine

⏱ Without -j (Single Thread)

Files are compiled one at a time. Estimated time: 40 to 60 minutes. Three CPU cores sit idle throughout the build. The bottleneck is that each file must wait for the previous one to finish.

1 job at a time
40-60 min on 4 cores
Wasteful of hardware

⚡ With -j8 (8 Parallel Jobs on 4 cores)

Eight compile processes run simultaneously on four cores. Different unrelated source files compile in parallel. Estimated time: 8 to 12 minutes — roughly 5 to 6 times faster.

8 jobs in parallel
8-12 min on 4 cores
Full CPU utilisation

⚠ Warning — Low RAM Systems

Each parallel compile job spawns a GCC or Clang process that consumes RAM. On a VM with less than 2 GB RAM, using all cores may trigger the OOM (Out of Memory) killer mid-build. If the build crashes silently, reduce the -j value to 2 or 4, or add swap space before building.

Build Prerequisites for Linux 6.x

Install these packages on Ubuntu or Debian before starting the build. Missing any of them causes the build to fail partway through, often deep into the compilation after you have already waited several minutes.

sudo apt install build-essential libncurses-dev libssl-dev \
  libelf-dev flex bison bc dwarves zstd

Why Each Package Is Required — Linux 6.x Specific

build-essential — Always required

Provides GCC, GNU make, and standard C headers. Without this nothing compiles at all.

libelf-dev — Required since kernel 4.15

Needed by objtool for stack validation (CONFIG_STACK_VALIDATION). Without it you see the warning “Cannot use CONFIG_STACK_VALIDATION, please install libelf-dev”.

dwarves (pahole) — Required since kernel 5.18, critical for 6.x

Generates BTF (BPF Type Format) data embedded in the kernel for BPF programs. The Linux 6.x build will fail completely with the error “pahole is not available” if this is not installed. This is the most common surprise for developers coming from older kernels.

zstd — Required since kernel 5.13

Module files are compressed with zstd by default. The build will fail or produce unusable modules without this package.

libssl-dev — Required since kernel 4.x

Supports kernel module signing. Needed whenever CONFIG_MODULE_SIG is enabled, which is the case in most distribution configs.

Files Produced After a Successful Build

Important Files After make -j$(nproc) Completes

vmlinux — Kernel Source Root

Uncompressed ELF kernel binary. Keep this for debugging. Do not copy to /boot — it is not a bootable file.

Keep for debugging ELF format Large file

System.map — Kernel Source Root

Symbol address table. Keep alongside vmlinux. Used by /proc/kallsyms and crash analysis tools.

Symbol table Keep with vmlinux

arch/x86/boot/bzImage — Copy This to /boot

The bootable compressed kernel. This is the file you copy to /boot/vmlinuz-6.6.30-mykernel and tell GRUB about.

Copy to /boot GRUB loads this Self-extracting

Scattered *.ko.zst files — Installed by Step 5

Hundreds of module files spread across drivers/, fs/, sound/, net/, etc. Step 5 (make modules_install) copies all of them to /lib/modules/.

Installed via modules_install zstd compressed

Common Build Errors in Linux 6.x and How to Fix Them

Linux 6.x Build Errors — Symptoms and Fixes

Error: Cannot use CONFIG_STACK_VALIDATION, install libelf-dev

Fix: sudo apt install libelf-dev

Error: pahole (pahole) is not available

Fix: sudo apt install dwarves — This is the most common blocker on 6.x for developers coming from older kernels.

Error: No rule to make target ‘debian/canonical-certs.pem’

Fix: Edit .config and set both CONFIG_SYSTEM_TRUSTED_KEYS="" and CONFIG_SYSTEM_REVOCATION_KEYS="". This happens when you use a distribution config on a different build environment.

Error: GCC version too old

Fix: Linux 6.x requires GCC 11+ or Clang 14+. Check with gcc --version and upgrade if needed: sudo apt install gcc-12.

Cleaning the Build Tree

# Remove compiled objects but keep .config
make clean

# Remove everything including .config — full reset
make mrproper

# Remove generated files, configs, and editor backup files
make distclean

💡 When to Use Which Clean Target

Use make clean when you want to force a full recompile without losing your configuration. Use make mrproper only when switching to a completely different kernel version or resetting everything from scratch — it deletes your .config file, so copy it somewhere safe first if you want to keep it.

📷 Suggested Featured Image

A terminal screenshot showing the very last lines of a successful kernel build ending with Kernel: arch/x86/boot/bzImage is ready (#1) is the perfect featured image for this post. It is authentic, immediately recognisable to Linux developers, and proves the tutorial outcome works.

Frequently Asked Questions

Q: How long does a Linux 6.x kernel build take?

On a modern 8-core machine with 16 GB RAM using make -j16, a full build typically takes 6 to 10 minutes. On a 2-core VM with 2 GB RAM using make -j4, expect 25 to 40 minutes. The first build is always the slowest. Subsequent incremental builds after small changes take only seconds to a few minutes because kbuild only recompiles changed files.

Q: Can I build the Linux kernel on a Raspberry Pi?

Yes, but it is slow on older models. A Raspberry Pi 5 with 4 GB RAM can build natively in around 30 to 45 minutes. For serious embedded development it is far more practical to cross-compile on your x86_64 development laptop. We cover cross-compilation for ARM in a later lecture in this free Linux kernel development course.

Q: What does the build number in “bzImage is ready (#5)” mean?

The number in parentheses tracks how many times this kernel source tree has been successfully built. It is stored in a counter file inside the build tree and increments with each successful build. It is useful when testing multiple iterations of a change so you can confirm which build is currently running without checking the full version string.

Interview Questions

Q1. What is the difference between vmlinux and bzImage?

vmlinux is the raw uncompressed ELF binary of the Linux kernel. It is not bootable but is essential for kernel debugging — tools like gdb and the crash utility need it to resolve function names from raw memory addresses. bzImage is a compressed, self-extracting kernel image. The bootloader (GRUB) loads bzImage into RAM and the kernel decompresses itself before starting. On x86_64, bzImage lives at arch/x86/boot/bzImage.

Q2. What does the -j flag in make do and what value should you choose?

The -j flag enables parallel compilation by specifying the maximum number of simultaneous jobs make can run. Without it, only one file compiles at a time regardless of how many CPU cores your machine has. The recommended value is two times the number of CPU cores. Using make -j$(nproc) automatically detects the core count, though strictly speaking $(nproc) gives the core count and you want twice that — make -j$(($(nproc)*2)) for optimal throughput on HyperThreaded systems.

Q3. What is a kernel module and how does it differ from built-in kernel code?

A kernel module is code compiled as a separate .ko.zst file that can be dynamically loaded into a running kernel and unloaded when no longer needed. Built-in code (config option set to Y) is compiled directly into vmlinux and is always present in memory from boot. Modules allow you to extend kernel functionality without rebooting and reduce memory footprint by only loading drivers for hardware that is actually present and in use.

Q4. What is System.map and what is it used for?

System.map is a text file generated during the kernel build that maps every kernel function and variable to its exact memory address in that specific build. The kernel’s /proc/kallsyms interface uses it for live symbol lookups. Crash analysis tools use it to translate raw hex stack trace addresses into human-readable function names. The addresses in System.map are build-specific — they change every time you compile.

Q5. What new build dependencies does Linux 6.x require that older 5.4 kernels did not?

Two critical additions: dwarves (pahole) — required since 5.18 for generating BTF (BPF Type Format) data. Without it the 6.x build fails completely, not just warns. Second, zstd — required since 5.13 because kernel modules are now stored as .ko.zst (zstd-compressed) files instead of uncompressed .ko files. Additionally, Linux 6.x mandates a modern compiler: GCC 11 or newer, or Clang 14 or newer.

Q6. What is the kbuild system?

kbuild is the Linux kernel’s build infrastructure — a layered system of Makefiles spread across the entire source tree. Every directory that contains compilable source has its own Makefile that lists exactly which files to build. The top-level Makefile coordinates everything. kbuild reads the .config file to decide what to include, handles cross-compilation transparently when you set ARCH and CROSS_COMPILE, and provides clean targets for resetting the build state.

Q7. What happens if you run make without first generating a .config file?

If no .config exists, kbuild cannot determine what to compile. Behaviour varies by kernel version: some versions abort with an explicit error, others may auto-generate a default config. Either way you should never rely on auto-generated defaults — always explicitly run a config step first (make defconfig, make menuconfig, or copy an existing config). A misconfigured build wastes time and may produce a kernel that does not boot on your hardware.

Continue the Free Linux Kernel Development Course

Next we install the compiled modules to /lib/modules/ and understand how the kernel finds and loads them at runtime.

← PREV_LEC
NEXT_LEC →

 

Leave a Reply

Your email address will not be published. Required fields are marked *