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
Topics Covered in This Lecture
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)
|
||||||||
|
↓ Kconfig scripts parse every option |
||||||||
|
||||||||
|
↓ objtool validates each object (stack & calling-convention checks) |
||||||||
|
||||||||
|
↓ Symbol table extracted |
||||||||
|
||||||||
|
↓ vmlinux compressed + boot stub prepended |
||||||||
|
||||||||
|
↓ Features configured as M compiled in parallel |
||||||||
|
||||||||
|
↓ MODPOST step — symbols verified & modules signed |
||||||||
|
||||||||
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/ | ||||||||||||||
|
|
|
||||||||||||
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
|
|
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
|
||||
|
↓ depmod reads each .ko |
||||
|
|
|||
|
↓ depmod resolves the full dependency graph |
||||
|
||||
|
↓ Written to modules.dep |
||||
|
||||
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
© EmbeddedPathashala • Free Embedded Systems and Linux Kernel Development Course • embeddedpathashala.com
