L29: Building the Linux 6.x Kernel & Installing Modules

 

Chapter 3 • Lecture 29 • Free Linux Kernel Development Course

Building the Linux 6.x Kernel & Installing Modules

Compile the kernel with parallel jobs — understand every output file — install your kernel modules correctly

Step 4Compile the Kernel
Step 5Install Modules
Linux 6.xUpdated for Latest
FreeEmbeddedPathashala

Topics Covered in This Lecture

make -j$(nproc)
kbuild system
bzImage
vmlinux
System.map
kernel modules .ko
make modules_install
INSTALL_MOD_PATH
depmod
parallel kernel build
module compression .ko.xz
linux 6.x build

Where We Are

In the previous lecture you configured the kernel using make menuconfig and produced a .config file that captures every build decision. Now comes the part that beginners find most satisfying — actually building the kernel and seeing it come to life.

This lecture covers two tightly connected steps. First you run the compiler and watch the kernel image get assembled piece by piece. Then you install the resulting kernel modules so the running system can find and load them. Everything here has been updated and verified against Linux 6.1 LTS, 6.6 LTS, and 6.12 stable.



Step 4 — Compiling the Kernel

Once your .config is in place, you start the build with a single make command. Because a kernel contains millions of lines of C code, you always want to use the parallel build option so multiple CPU cores compile different files at the same time.

The Correct Build Command for Linux 6.x

Old tutorials show make -j4 with a hardcoded number. The modern and correct approach is:

cd /path/to/linux-6.x        # enter your kernel source directory
make -j$(nproc)              # use ALL available CPU threads

The shell evaluates $(nproc) at runtime. It returns the number of logical CPU threads on your machine — 8 on a quad-core with hyperthreading, 16 on a 16-thread workstation, and so on. This way the build always uses every available core without you having to change the command when you move machines.

📷 Image Suggestion — Terminal showing make -j$(nproc) running

A screenshot or screen recording of a terminal window running make -j$(nproc) inside a Linux 6.x source tree, showing the compiler output lines scrolling with CC, LD, MODPOST entries. Helps readers know what a healthy build looks like in practice.

Running on a Virtual Machine?

⚠ Building on a VM is slower — here is how to help it

A kernel build is one of the most CPU and RAM intensive tasks you can run. Inside a VM your available cores and memory are shared with the host OS. Two things help significantly. Boot the VM into a text-only runlevel (no desktop environment running) before starting the build — this frees RAM. Also allocate as many vCPUs to the VM as your host will allow in the VM settings.



What Actually Happens When You Run make

The Linux build system is called kbuild. It reads your .config and orchestrates the compilation of every enabled subsystem in the correct order. The diagram below shows the high-level flow from your config file to the final bootable image.

Linux Kernel Build Flow — kbuild (Linux 6.x)

.config file

↓ Kconfig scripts parse every option

arch/
CPU & arch code
drivers/
device drivers
fs/
filesystems
net/
networking
Each subsystem compiles its .c files into .o object files in parallel

↓ objtool validates each object (stack & calling-convention checks)

All .o files linked together
vmlinux — uncompressed ELF image

↓ Symbol table extracted

System.map
symbol name → memory address

↓ vmlinux compressed + boot stub prepended

arch/x86/boot/bzImage
The bootable kernel (8–15 MB)

↓ Features configured as M compiled in parallel

Kernel Modules
driver.ko   fs.ko   net.ko

↓ MODPOST step — symbols verified & modules signed

Build complete
Kernel: arch/x86/boot/bzImage is ready (#1)



Three Key Output Files You Must Know

A successful kernel build produces many files, but three of them matter most. Every Linux kernel developer should be able to explain each one without hesitation.

Key Output Files After a Successful Kernel Build

File Location What It Is Keep It?
vmlinux Kernel source root Uncompressed ELF kernel. Very large (400–600 MB). Contains every symbol and debug section. Used by GDB and crash tools for debugging. You do not boot from this. Yes — for debugging
System.map Kernel source root Plain text file. Maps every kernel function and variable name to its virtual memory address. Used during debugging to decode raw addresses in kernel panics. Also used by the kernel itself for symbol resolution at boot. Yes — keep with vmlinux
bzImage arch/x86/boot/bzImage The compressed, bootable kernel image. This is what GRUB or systemd-boot loads when your machine starts. Typically 8–15 MB. The name bz historically stood for big-zImage, not bzip2 — modern kernels use gzip or zstd internally. Yes — this is what you boot

📷 Image Suggestion — File size comparison diagram

A simple visual comparing vmlinux (~500 MB), System.map (~4 MB), and bzImage (~10 MB) as horizontal bars. Helps students immediately grasp why vmlinux is only for debugging and is never placed in /boot on a production system.

Architecture-Specific Image Names in Linux 6.x

💡 The image name changes depending on CPU architecture

On x86 and x86_64 the image is always bzImage at arch/x86/boot/bzImage. On ARM64 it is arch/arm64/boot/Image.gz. On RISC-V it is arch/riscv/boot/Image.gz. When cross-compiling for embedded targets you need to know the correct path. You can always find it with make -s image_name from the kernel source directory.

# Find the correct image path for whatever arch you configured
make -s image_name

# Examples of what this prints:
# x86_64   ->  arch/x86/boot/bzImage
# ARM64    ->  arch/arm64/boot/Image.gz
# RISC-V   ->  arch/riscv/boot/Image.gz



The Build Counter — What Does (#1) Mean?

At the very end of a successful build you see this line:

Kernel: arch/x86/boot/bzImage is ready  (#1)

The number in parentheses is a build counter maintained by kbuild. It starts at 1 for the first ever build of that kernel source tree and increments automatically with every subsequent rebuild. When you boot into your custom kernel and run uname -a, this counter appears embedded in the version string. This makes it easy to know exactly which rebuild you are currently running, which is very useful when you are iterating on kernel code and rebuilding multiple times a day.



How Long Does a Kernel Build Take?

Approximate Build Times for Linux 6.x (full kernel, not localmodconfig)

Machine Cores / Threads Approximate Time
Modern developer laptop (native Linux) 8 cores / 16 threads 10 – 20 minutes
VM with 4 vCPUs assigned 4 threads 25 – 45 minutes
Cloud build server (16+ cores) 16+ threads 5 – 10 minutes
Raspberry Pi 4 (native ARM, cross-compile target) 4 cores 60 – 90 minutes

⚡ Speed tip — Use make localmodconfig

For development and learning, you almost never need to build every possible driver. Run make localmodconfig before building to trim the .config down to only what is running on your current system. The resulting build can be three to five times faster and produces far fewer .ko files. This is the approach most kernel contributors use day-to-day.



Step 5 — Installing the Kernel Modules

Every kernel feature you set to M in menuconfig gets compiled into a separate .ko (Kernel Object) file. At the end of make -j$(nproc) these files exist inside the kernel source tree but the running system cannot reach them there. The module loading tools — modprobe, insmod, and the kernel itself — all expect modules to live in one specific place on the filesystem.

Step 5 copies every .ko file to that location and builds the dependency database that makes module auto-loading work.

Where Do Modules Live After Installation?

The standard system-wide location for kernel modules is:

/lib/modules/<kernel-version>/

For example, if you built a 6.6.30 kernel with a custom suffix -dev set in menuconfig, the modules go to /lib/modules/6.6.30-dev/. Inside that directory, kbuild recreates the exact same subdirectory hierarchy that exists in the source tree, so drivers/hid/usbhid/usbhid.ko in the source becomes /lib/modules/6.6.30-dev/kernel/drivers/hid/usbhid/usbhid.ko (or its compressed variant) after installation.



Module Installation Layout — /lib/modules/<version>/

/lib/modules/6.6.30-dev/
kernel/
all .ko files
drivers/net/...
drivers/usb/...
fs/fat/...
sound/pci/...
Module database
modules.dep
dependency graph
modules.alias
device ID mappings
modules.order
load ordering
Built-in info
modules.builtin
features compiled =y
modules.builtin.modinfo
metadata for builtins

Running the Install Command

sudo make modules_install

You need sudo because writing to /lib/modules/ requires root. The command does three things in sequence: copies all .ko files to /lib/modules/<version>/kernel/, generates the module database files (modules.dep, modules.alias, etc.) by running depmod automatically, and prints a summary of what was installed.

💡 Linux 6.x note — Module Compression

In Linux 6.x many distributions and custom builds enable module compression. When active, .ko files are compressed and stored as .ko.xz (xz compression) or .ko.zst (zstd compression). This reduces the space used by /lib/modules/ significantly. Tools like modprobe and insmod handle decompression transparently — you never deal with compressed modules manually. The kernel build selects the compression algorithm based on your .config settings under General Setup → Kernel compression mode.



INSTALL_MOD_PATH — For Embedded and Cross-Compilation Builds

If you are cross-compiling a kernel for an embedded target board, you never want to install modules into your host machine’s /lib/modules/. The host kernel and the target kernel are completely different. The INSTALL_MOD_PATH variable solves this by redirecting installation to a staging directory of your choice.

# Install to a staging directory instead of the host /lib/modules
sudo make INSTALL_MOD_PATH=/tmp/my-rootfs modules_install

# Modules are now at:
# /tmp/my-rootfs/lib/modules/6.6.30-dev/kernel/...

You then package that staging directory into a root filesystem image (a tarball, an ext4 image, a CPIO archive) and deploy it to the target board. This is the standard workflow in every professional embedded Linux project.

Module Install — Native vs Embedded Cross-Compilation Flow

Native Build
host = target
make modules_install
/lib/modules/6.x/
on the same machine
modprobe loads modules
immediately on reboot
Cross / Embedded Build
host ≠ target
make INSTALL_MOD_PATH=
/tmp/staging modules_install
/tmp/staging/lib/modules/
on host machine
Package into rootfs image
Flash / deploy to target board



What Is depmod and Why Does It Run Automatically?

After copying the .ko files, make modules_install runs depmod automatically. This is not optional. Without it, modprobe would not know how to load your modules.

Here is the problem depmod solves. Kernel modules can depend on each other. A USB audio driver might need the core USB host controller module to be loaded first, which itself needs the USB core module. When you ask modprobe to load the USB audio driver, it must know this chain automatically and load dependencies in the right order. depmod figures all of this out by reading the exported and imported symbols from every .ko file and writing the result to modules.dep.

How depmod Builds the Module Dependency Database

All .ko files in
/lib/modules/<ver>/kernel/

↓ depmod reads each .ko

EXPORT_SYMBOL()
What this module provides to others
 
Undefined symbols
What this module needs from others

↓ depmod resolves the full dependency graph

snd-usb-audio.ko depends on snd-pcm.ko
snd-pcm.ko depends on soundcore.ko
usbhid.ko depends on hid.ko

↓ Written to modules.dep

modprobe reads modules.dep
Loads correct chain automatically



Verifying the Installation

# List all kernel version directories on the system
ls /lib/modules/

# Your newly built version should appear, for example:
# 6.6.30-dev

# Check what was installed inside it
ls /lib/modules/6.6.30-dev/

# Count installed module files
find /lib/modules/6.6.30-dev/ -name "*.ko*" | wc -l

# Confirm the dependency database was generated
ls /lib/modules/6.6.30-dev/modules.dep



Complete Build and Install Sequence — Linux 6.x

# Step 1: Enter kernel source directory
cd /path/to/linux-6.x

# Step 2: Configure (covered in previous lecture)
make menuconfig

# Step 3: Build the kernel and all modules in parallel
make -j$(nproc)

# Step 4: Install kernel modules to system module directory
sudo make modules_install

# Step 5: Install kernel image and update bootloader (next lecture)
sudo make install
# OR on Debian / Ubuntu:
# sudo cp arch/x86/boot/bzImage /boot/vmlinuz-6.x.y-custom
# sudo cp System.map /boot/System.map-6.x.y-custom
# sudo update-initramfs -c -k 6.x.y-custom
# sudo update-grub

⚠ Never skip make modules_install before rebooting

A very common beginner mistake is to copy only the bzImage to /boot and reboot into the new kernel. When it boots, it looks for modules in /lib/modules/<new-version>/ and finds nothing there. The result is missing drivers at runtime — no network, no USB, no sound. Always run make modules_install first, then reboot.



Frequently Asked Questions

Why is the vmlinux file so huge compared to bzImage?

vmlinux is an uncompressed ELF binary that contains every compiled kernel function, variable, and their debug symbols. It can easily be 400 to 600 MB. bzImage is produced by stripping the debug sections, compressing the remaining code and data, and prepending a small decompressor stub. The decompressor runs before the kernel proper starts at boot time, expands the kernel into RAM, and passes control to it. This is why bzImage is only 8 to 15 MB while containing essentially the same kernel code.

Can I run make modules_install and make install without sudo?

No, unless you are using INSTALL_MOD_PATH or INSTALL_PATH to redirect to a directory your user owns. Installing to /lib/modules/ and /boot/ both require root privilege because they are system directories. In embedded development workflows you almost always use INSTALL_MOD_PATH pointing to a staging area you own, which avoids the need for sudo entirely.

What happens when I rebuild the kernel a second time?

kbuild uses timestamps and dependency tracking. On a rebuild, only the files whose source has changed (or whose dependencies changed) are recompiled. This is called an incremental build and is much faster than a full build. The build counter increments from #1 to #2. You then run make modules_install again to update /lib/modules/ with the new .ko files.

What is Module.symvers and when do I need it?

Module.symvers is generated in the kernel source root at the end of the build. It lists every exported symbol from the kernel and all in-tree modules, along with CRC checksums used by module versioning (CONFIG_MODVERSIONS). When you later build an out-of-tree kernel module (an external driver) against your custom kernel, the build system reads Module.symvers to verify that the symbols your module depends on actually exist and have not changed. Without this file, out-of-tree module builds fail with undefined symbol errors.

My build failed with errors. What should I do?

First, read the error message carefully. The most common causes are: a kernel configuration conflict (two options that cannot both be enabled), a toolchain version mismatch (GCC too old or too new for the kernel version), a missing build dependency (a header or library the kernel needs), or a bad patch applied to the source. If you cannot identify the cause, run make mrproper to completely clean the source tree (this deletes your .config too, so back it up first), reconfigure from scratch, and build again.

What does .ko.xz mean in Linux 6.x module directories?

In Linux 6.x, module compression is commonly enabled in the kernel configuration. When active, make modules_install compresses each .ko file using xz compression before writing it to /lib/modules/. The result is .ko.xz files instead of plain .ko files. Zstd compression produces .ko.zst files. Both formats are handled transparently by modprobe, insmod, and depmod. You never need to manually decompress them.



Interview Questions — Kernel Build and Module Installation

Q1. What does make -j$(nproc) do and why is it preferred over make -j4?

make -j$(nproc) tells the make utility to run as many parallel compilation jobs as there are logical CPU threads on the machine. $(nproc) is evaluated by the shell at runtime and returns the actual thread count. This is better than hardcoding -j4 because it scales automatically to whatever machine you are on. A 16-thread machine uses 16 parallel jobs; a 4-thread VM uses 4. Hardcoding wastes available parallelism on powerful machines and would need to be changed manually every time you switch systems.

Q2. What are vmlinux and bzImage and how do they differ?

vmlinux is the uncompressed kernel image in ELF format. It is very large (400 to 600 MB) because it contains all kernel symbols and full debug information. You use it with GDB and the crash utility for post-mortem debugging. It is not directly bootable. bzImage is the compressed, bootable kernel image. The kbuild system takes vmlinux, strips debug sections, compresses the remaining code, and prepends a boot stub. The result is 8 to 15 MB and is what the bootloader loads from /boot/.

Q3. What is System.map and why must you keep it?

System.map is a plain text file mapping every kernel symbol (function name, variable name, exported interface) to its virtual memory address. During debugging, when a kernel panic prints a raw hex address, you look it up in System.map to find the exact function that was executing. The kernel also reads it at boot for its internal symbol resolution. Always keep the System.map file that was generated alongside the vmlinux it corresponds to. A System.map from a different build is useless because addresses change with every compilation.

Q4. What does the .ko extension mean and where do modules end up after installation?

.ko stands for Kernel Object. It is a relocatable ELF binary implementing a loadable kernel module, corresponding to any feature set to M in menuconfig. After make modules_install runs, all .ko files are copied to /lib/modules/<kernel-version>/kernel/ preserving the source tree subdirectory structure. On Linux 6.x with compression enabled they are stored as .ko.xz or .ko.zst.

Q5. What does depmod do and why does make modules_install run it automatically?

depmod reads every .ko file installed in /lib/modules/<version>/, extracts the symbols each module exports and needs, and builds a dependency graph. It writes the result to modules.dep, modules.alias, and related files. make modules_install runs depmod automatically at the end because without these files, modprobe cannot determine which modules to load first when you request a specific driver. The dependency database is what enables automatic dependency loading.

Q6. What is INSTALL_MOD_PATH and when do embedded engineers use it?

INSTALL_MOD_PATH is a make variable that redirects module installation from the system’s /lib/modules/ to any directory you specify. Usage: make INSTALL_MOD_PATH=/tmp/staging modules_install. Embedded Linux engineers use this when cross-compiling a kernel for a target board. You do not want the ARM modules inside your x86 host’s /lib/modules/. Instead you stage them to a directory, then package that directory into the target’s root filesystem image.

Q7. What is the build counter that appears after a kernel build and what does it track?

The line Kernel: arch/x86/boot/bzImage is ready (#1) at the end of a build contains a build counter. It starts at 1 for the very first build of a given source tree and increments with every rebuild. The counter is stored in a file inside the kernel source tree. When you boot into your custom kernel and run uname -a, this number appears in the version string. During active development where you rebuild multiple times a day, the counter helps you immediately confirm which exact rebuild is currently running.

Q8. What happens if you boot a new kernel but skip make modules_install?

The new kernel boots and looks for its modules in /lib/modules/<new-version>/. Because you skipped the install step, that directory does not exist or contains modules from a different kernel version. Any feature compiled as a module will be completely unavailable. Depending on your configuration this means no network interface (if the NIC driver is a module), no USB, no sound, and possibly no filesystem support. This is one of the most common mistakes beginners make when building their first custom kernel.

Q9. What is Module.symvers and when do out-of-tree module developers need it?

Module.symvers is generated in the kernel source root during the build. It lists every exported symbol from the kernel and all in-tree modules along with CRC checksums used by CONFIG_MODVERSIONS. When you build an out-of-tree kernel module (a third-party driver or your own module being developed outside the kernel tree), the build system reads Module.symvers to verify that every symbol your module depends on actually exists in the target kernel and has not changed its interface. Without this file, out-of-tree module builds fail with undefined symbol errors during the MODPOST step.

Continue Your Free Linux Kernel Development Course

EmbeddedPathashala — Free courses for embedded systems and Linux kernel developers

Back to Course Index

© EmbeddedPathashala • Free Embedded Systems and Linux Kernel Development Course • embeddedpathashala.com

 

Leave a Reply

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