Linux Kernel Build – Practical Tips-Linux Device Driver Training Online

EmbeddedPathashala › Linux Kernel Programming › Chapter 3 – Practical Kernel Build Tips
Linux Kernel Build – Practical Tips
Chapter 3 | Kernel 6.x Updated | Free Linux Kernel Development Course
⏱ 20 min read
🎯 Beginner – Intermediate
🐧 Kernel 6.x
Topics covered:
Kernel Packaging deb / rpm build V=1 Verbose Build Build Scripts Compiler Flags Toolchain Requirements Cross Compilation Kernel 6.x

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.

Kernel & Root Filesystem – Loose Coupling
🐧
Linux Kernel
vmlinuz-6.x
Can be swapped freely
⟷
📁
Root Filesystem
/bin, /lib, /etc, apps…
Stays unchanged
The kernel and root filesystem are independently replaceable. One system image can run multiple kernels.

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.

💡 Tip: On production embedded boards, teams often keep multiple kernels in the boot partition — for example 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:

Kernel 6.x – Minimum Toolchain Requirements
Tool Min Version (kernel 6.x) Why it matters
gcc5.1 or laterPrimary C compiler for the kernel
clang11.0 or laterAlternative compiler; needed for LLVM/Clang builds
make3.82 or laterThe kernel uses many advanced GNU Make features
binutils2.25 or laterAssembler, linker, and binary utilities
flex / bison2.5.35 / 2.3Used by Kconfig and other kernel tools
python33.5.3 or laterKconfig tools and build scripts
openssl1.0.0 or laterKernel module signing, secure boot support
pahole1.16 or laterRequired for BTF/BPF support (new in kernel 5.2+)
📌 Kernel 6.x change: The minimum gcc version is now 5.1 — the old reference of gcc 4.9 no longer applies. Also, the 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.

Kernel Packaging Workflow
🖥️
Build Machine
x86_64 workstation
→
make bindeb-pkg
📦
.deb Package
kernel + modules + headers
→
dpkg -i
🎯
Target System
remote / embedded board

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:

Generated .deb Packages – What Each Contains
linux-image-*.deb
The compressed kernel binary (vmlinuz) and the System.map file. This is the actual kernel you boot into.
linux-headers-*.deb
Kernel header files. Required when building out-of-tree kernel modules against this specific kernel version.
linux-libc-dev_*.deb
Kernel-exported headers for userspace. Needed when building glibc or other low-level system libraries.

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
💡 Kernel 6.x note: With debug info enabled, you may also see a 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.
⚠️ Architecture matching: A packaged kernel can only be installed on machines with the same CPU architecture. An 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
Normal Build vs V=1 Verbose Build
Normal Output (default)
CC kernel/sched/core.o CC kernel/fork.o CC kernel/pid.o LD vmlinux OBJCOPY arch/x86/boot/vmlinux.bin
Verbose Output (V=1)
gcc -Wp,-MMD,kernel/sched/.core.o.d -nostdinc -I./arch/x86/include -D__KERNEL__ -Wall -O2 -fno-PIE -fno-strict-aliasing -c -o kernel/sched/core.o kernel/sched/core.c
V=1 shows every compiler flag — invaluable for debugging build failures

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
📌 Kernel 6.x – V=2 also available: 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
Bash Conditional Execution – How && and || Work
cmd1 && cmd2
Run cmd2 only if cmd1 succeeds (exit code 0). If the compile step fails, modules_install is automatically skipped — preventing a bad install.
cmd1 || cmd2
Run cmd2 only if cmd1 fails. Useful for fallback actions, sending error notifications in CI, or running cleanup steps on failure.
💡 Use $(nproc) instead of -j4: $(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
The -fPIE Conflict – Why It Happens
🔧
System gcc
Built with -fPIE on by default (security hardening for userspace apps)
✕
🐧
Linux Kernel
Requires -fno-PIE — it has its own fixed virtual load address
Result → “code model kernel does not support PIC mode”

The 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
📌 General debugging rule: Whenever a build error points to a generic file like 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.

Q1. After building and booting a new Linux kernel from source, why does the root filesystem remain unchanged?
The Linux kernel and root filesystem are loosely coupled — completely independent components. The kernel handles hardware abstraction and system calls. The root filesystem holds applications, libraries, and configuration. Replacing the kernel leaves the filesystem on disk untouched. This design also allows the same root filesystem to be used with multiple different kernels — such as a standard build and a real-time patched build.
Q2. What is the minimum gcc version required for kernel 6.x, and where is the authoritative list of toolchain requirements?
The minimum required gcc version for kernel 6.x is 5.1 (raised from 4.9 in older kernels). The authoritative, always up-to-date list is in the kernel source itself at Documentation/process/changes.rst. It covers gcc, clang, make, binutils, flex, bison, Python 3, OpenSSL, and pahole.
Q3. You need to deploy a custom-built kernel to 50 remote target machines. What is the best approach?
Use the kernel’s built-in packaging support. Running 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.
Q4. How do you get more diagnostic information when a kernel build fails?
Add 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.
Q5. What does the -fPIE compiler flag mean, and why does the Linux kernel require it to be disabled?
PIE stands for Position Independent Executable. With -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.
Q6. Why is -j$(nproc) preferred over a hardcoded value like -j4 in kernel build commands?
$(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.
Q7. What is the difference between make bindeb-pkg and make deb-pkg?
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
© EmbeddedPathashala | Free Embedded Systems & Linux Kernel Development Course | All kernel information verified against kernel 6.x documentation.

2 Comments

Leave a Reply

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