📋 Table of Contents
If you have been following along in this chapter, you now know how to download the kernel source, configure it, and compile it — including cross-compiling for an embedded board like a Raspberry Pi. In this final section of Chapter 3, we look at a handful of practical tips that come in real-world kernel development. These are things that many beginners miss, yet they make your workflow much smoother once you know them.
The reference material for this chapter was written for an older kernel (5.4.x era). Where things have changed in the current stable kernel (6.x), we have updated the information accordingly. Always verify details against the kernel documentation that ships with your specific kernel version.
1. Why Only the Kernel Changes After a Rebuild
New developers are often surprised by this: you compile and boot a brand-new kernel — but when you look at the filesystem, the applications, and the libraries, everything is exactly the same as before. Nothing changed except the kernel binary itself. Is that expected?
Yes, absolutely. This is by design. Unix deliberately keeps the kernel and the root filesystem loosely coupled. Think of it like a car engine and the cabin — swapping the engine does not change anything on the dashboard or the seats. The kernel handles hardware abstraction, process management, and system calls. The root filesystem holds your applications, libraries, and configuration files. They are independent.
This loose coupling gives embedded product teams a major advantage. You can maintain a single root filesystem while shipping different kernel builds — for example, a standard kernel and a real-time patched kernel — all on the same base system.
vmlinuz-6.1.0-default and vmlinuz-6.1.0-rt (real-time). The bootloader selects which one to load. The root filesystem is identical in both cases.
2. Minimum Toolchain Version Requirements Updated for 6.x
Before you attempt to build the Linux kernel, make sure your build machine has the required minimum versions of all the tools the kernel build system needs. Using an outdated compiler or an old version of make will result in cryptic build errors that are hard to debug.
The kernel’s own documentation is the single best source of truth for this. You can find the requirements inside the kernel source tree at Documentation/process/changes.rst. Here is a summary of what the kernel 6.x series expects:
| Tool | Min Version (kernel 6.x) | Why it matters |
|---|---|---|
| gcc | 5.1 or later | Primary C compiler for the kernel |
| clang | 11.0 or later | Alternative compiler; needed for LLVM/Clang builds |
| make | 3.82 or later | The kernel uses many advanced GNU Make features |
| binutils | 2.25 or later | Assembler, linker, and binary utilities |
| flex / bison | 2.5.35 / 2.3 | Used by Kconfig and other kernel tools |
| python3 | 3.5.3 or later | Kconfig tools and build scripts |
| openssl | 1.0.0 or later | Kernel module signing, secure boot support |
| pahole | 1.16 or later | Required for BTF/BPF support (new in kernel 5.2+) |
dwarves package (which provides pahole) is now required when BPF or BTF debug info is enabled, which is the default in most distro configs. Install it with: sudo apt install dwarves
Check what you have installed:
gcc --version
make --version
ld --version
flex --version
bison --version
python3 --version
pahole --version
Install all essential build dependencies on Ubuntu/Debian in one command:
sudo apt install build-essential libncurses-dev bison flex \
libssl-dev libelf-dev dwarves
3. Building the Kernel as a Package (deb / rpm) Updated for 6.x
Here is a scenario you will commonly face in professional embedded development: your team builds the kernel on a powerful x86 workstation, but the kernel needs to run on dozens of target boards at a customer site. How do you get the kernel, its modules, headers, and all associated files onto those remote machines cleanly and reliably?
The answer is kernel packaging. The kernel build system has built-in support for generating standard Linux packages — .deb for Debian/Ubuntu-based systems and .rpm for Red Hat/Fedora-based systems. Once packaged, you can install it on any matching target machine using the standard package manager.
See all available packaging targets:
make help
To build only the binary .deb packages (the most common choice):
make -j$(nproc) bindeb-pkg
The generated packages appear one level above the kernel source directory. You typically get three packages:
To install on the target machine:
# Copy .deb files to the target, then run:
sudo dpkg -i linux-image-*.deb linux-headers-*.deb linux-libc-dev_*.deb
# Update the bootloader so the new kernel appears at boot
sudo update-grub
linux-image-*-dbg.deb package. This contains unstripped kernel symbols for debugging and can be several gigabytes. Skip it unless you specifically need kernel-level debugging with tools like crash or gdb.
amd64 kernel package cannot be installed on an ARM64 board. For cross-compiled embedded targets, use the ARCH and CROSS_COMPILE variables together with bindeb-pkg — the resulting package will be correctly marked with the target architecture.
4. Watching the Build in Detail – V=1
By default, the kernel build output is intentionally terse. You see short clean lines like CC kernel/sched/core.o or LD vmlinux. This is fine during normal builds. But when something fails — especially with a cryptic compiler error — you need to see the exact compiler command that was run, including every flag.
The V=1 switch tells the build system to print the full command lines:
# Native x86 verbose build
make V=1 all
# Cross-compile verbose build for ARM64
make V=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules dtbs
The verbose output is especially useful when:
- A module fails to compile and the error message alone is not clear enough
- You suspect the wrong compiler or wrong flags are being picked up
- You are debugging a cross-compiler toolchain issue
- You want to understand how the kernel Makefile system works internally
V=2 additionally prints the reason why each target is being rebuilt — which file changed and triggered the recompile. Use it when incremental builds seem to be rebuilding more than expected.
5. Shortcut Build Script for Automation
Once the kernel configuration step is done, the full build workflow has three parts: compile, install modules, install the kernel. When you are iterating rapidly or running automated CI builds, chaining these into a single command using Bash’s conditional operators saves time and prevents mistakes.
Full native x86 build and install:
time make -j$(nproc) all \
&& sudo make modules_install \
&& sudo make install
Cross-compiled build for an ARM64 embedded target:
time make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules dtbs \
&& sudo make ARCH=arm64 INSTALL_MOD_PATH=/path/to/rootfs modules_install
cmd2 only if cmd1 succeeds (exit code 0). If the compile step fails, modules_install is automatically skipped — preventing a bad install.cmd2 only if cmd1 fails. Useful for fallback actions, sending error notifications in CI, or running cleanup steps on failure.$(nproc) automatically returns the number of available CPU threads on the current machine, so the build scales to whatever hardware you are on — a laptop, a workstation, or a CI cloud instance with varying core counts.
6. Dealing with Compiler Flag Issues
Every now and then, you will hit a build failure that has nothing to do with the kernel code itself — it is caused by a mismatch between what the kernel’s Makefile expects from the compiler and what your system’s default compiler configuration provides. These issues typically appear when using a very new or very old Linux distribution, or when your distro’s gcc has been compiled with non-standard default flags.
A classic example is the -fPIE problem. Some Ubuntu releases configure gcc to compile everything with Position Independent Executable support (-fPIE) on by default — a userspace security hardening technique. The Linux kernel, however, manages its own memory layout and cannot be compiled as a position-independent executable. When this conflict occurs, you see an error like:
scripts/mod/empty.c:1:0: error: code model kernel does not support PIC mode
-fPIE on by default (security hardening for userspace apps)-fno-PIE — it has its own fixed virtual load addressThe good news: kernel 6.x has fully fixed this. The kernel’s top-level Makefile explicitly sets -fno-PIE and -fno-pic, overriding whatever the system gcc defaults are. If you are building kernel 6.x on a modern Ubuntu/Debian system, you will not encounter this error.
However, if you maintain a product that must run an older kernel (4.x or early 5.x LTS) on a modern build machine, you may still hit this. The workaround is:
# Workaround for older kernels on distros that default to -fPIE
make CFLAGS="-fno-PIE" all
scripts/mod/empty.c rather than an actual kernel source file, suspect a toolchain configuration issue first. Run make V=1 to see the full compiler flags — the problem usually becomes immediately obvious.
🎯 Interview Questions – Kernel Build Tips
Commonly asked in embedded Linux and kernel development interviews. Practice these in your own words.
Documentation/process/changes.rst. It covers gcc, clang, make, binutils, flex, bison, Python 3, OpenSSL, and pahole.make -j$(nproc) bindeb-pkg generates standard .deb packages containing the kernel image, headers, and libc-dev. These can be installed on any matching machine with dpkg -i or through an internal package repository. For Red Hat targets, use make binrpm-pkg. This approach is more reliable than manually copying files because the package manager handles installation paths, boot configuration, and dependency tracking automatically.V=1 to the make command: make V=1 all. This switches the kernel build to verbose mode, printing every compiler and linker command with the complete set of flags and arguments. This immediately reveals whether wrong include paths, incorrect optimization flags, or unexpected compiler options are responsible for the failure. V=2 additionally shows why each file is being rebuilt.-fPIE enabled, the compiler generates code that can be loaded at any memory address — a security hardening technique for userspace applications. The Linux kernel, however, uses a fixed virtual load address and manages its own memory layout, especially during early boot. It cannot be compiled as a PIE binary. Kernel 6.x explicitly sets -fno-PIE in its Makefile, overriding any system default, so this issue no longer affects modern kernel builds.$(nproc) returns the number of available CPU threads on the current machine at runtime. Using it makes your build command portable — it automatically uses all available cores whether you run it on a 4-core laptop, a 32-core server, or a CI instance with varying vCPUs. Hardcoding -j4 means underutilizing a more powerful machine or overloading a weaker one.make bindeb-pkg produces only the binary .deb packages — the kernel image, headers, and libc-dev — which are what you need to install on a target machine. make deb-pkg produces both binary packages and a source package containing the entire kernel source tree. The source package is useful for publishing to a PPA or distributing your kernel source. For deployment purposes, bindeb-pkg is sufficient and significantly faster.Continue Learning – Free Linux Kernel Course
EmbeddedPathashala offers completely free, student-friendly tutorials on Linux kernel programming, embedded systems, and Bluetooth/BLE development.
Visit EmbeddedPathashala
2 Comments