The Kbuild System — Building Kernel Modules Like a Pro-Free Linux kernel development course

Previous Lecture
Next Lecture

Linux Kernel Programming
Chapter 4  |  Part 6 of 6
The Kbuild System — Building Kernel Modules Like a Pro
📚 Intermediate
⏱ ~25 min
🔐 Kernel 6.x

🔍 Topics Covered
Kbuild System
obj-m vs obj-y
Multi-file Modules
EXTRA_CFLAGS
Cross Compilation
ARCH and CROSS_COMPILE
Kconfig Symbols
ccflags-y
Install a Module

🎯 Understanding the Build System Saves Hours of Pain

Many beginners can write kernel module code but struggle when the build breaks in unexpected ways. The Linux kernel uses a sophisticated build system called Kbuild, and understanding how it works — even at a basic level — will save you enormous amounts of debugging time. This tutorial goes beyond the simple “copy-paste Makefile” approach and explains what is actually happening.

🏗 What is Kbuild?

Kbuild is the build system used to compile the Linux kernel and all its modules. It is built on top of GNU Make but adds many layers of conventions, automatic dependency tracking, and cross-compilation support on top.

When you run make in your module’s directory, here is the high-level flow:

How an Out-of-Tree Module Gets Built
1
You run make in your module directory. Your outer Makefile runs and it changes directory (-C) into the kernel build tree at /lib/modules/$(uname -r)/build.
2
The kernel’s top-level Makefile takes over. It reads the kernel configuration and sets up the full build environment (compiler flags, architecture, include paths, etc.).
3
The kernel’s Makefile then re-invokes make, this time pointing back at your module directory (M=$(PWD)). Now it reads your Makefile a second time — but this time from inside the Kbuild environment.
4
Kbuild reads your obj-m assignments, compiles your source files with the exact same compiler flags used to build the kernel itself, and links the .ko file.
5
The finished module.ko file appears in your directory, with correct vermagic embedded.

This two-pass approach is why your Makefile gets read twice: once to kick off the kernel build, and once from inside the kernel build environment to tell it what to build. Some advanced Makefiles detect which pass they are in using the KERNELRELEASE variable (set only during the second pass).

📄 Makefile Anatomy — The Professional Version

Here is a more complete Makefile that handles both the two-pass situation and common build needs:

# ==========================================================
# Professional Out-of-Tree Kernel Module Makefile
# ==========================================================

# Name of the module to build
MODULE_NAME := mydriver

# Source files that make up the module
# (for single file: just obj-m += mydriver.o)
obj-m := $(MODULE_NAME).o

# If your module has multiple .c files, list them:
# mydriver-objs := main.o gpio.o irq.o i2c.o

# Optional: Add custom compiler flags
# ccflags-y += -DDEBUG_MODE
# ccflags-y += -I$(src)/include

# Kernel build directory — default to running kernel
KDIR ?= /lib/modules/$(shell uname -r)/build

# Module source directory — default to current directory
PWD := $(shell pwd)

# ==========================================================
# KERNELRELEASE is set by the kernel build system.
# If it is set, we are in the second pass (inside Kbuild).
# If it is empty, we are in the first pass (user invoked).
# ==========================================================

ifneq ($(KERNELRELEASE),)
  # Second pass: Kbuild handles everything
  obj-m := $(MODULE_NAME).o

else
  # First pass: kick off the kernel build

  # Default target
  all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

  # Install the module into /lib/modules/$(uname -r)/extra/
  install:
	$(MAKE) -C $(KDIR) M=$(PWD) modules_install
	depmod -a

  # Clean all build artifacts
  clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

  # Show module information after build
  info:
	modinfo $(MODULE_NAME).ko

endif

🔣 obj-m, obj-y, and obj-n — The Three Assignment Types

In the Kbuild system, the assignment to obj- variables controls whether code is compiled as a module, built-in, or excluded:

# obj-m: compile as a loadable kernel module (.ko)
obj-m += mydriver.o

# obj-y: compile as built-in kernel code (part of vmlinux)
obj-y += core_feature.o

# obj-n: do NOT compile (N = No)
# (This is the default for undefined variables)

# In Kconfig-driven builds, you often see:
obj-$(CONFIG_MY_DRIVER) += mydriver.o
# If CONFIG_MY_DRIVER=m  → compiled as module
# If CONFIG_MY_DRIVER=y  → compiled built-in
# If CONFIG_MY_DRIVER=n  → not compiled at all

The obj-$(CONFIG_...) pattern is what the kernel’s own Makefiles use. It makes features automatically respond to the user’s make menuconfig choices.

📄 Multi-File Kernel Modules

Real-world drivers are rarely a single file. When your module grows, you split it into multiple .c files. Kbuild handles this cleanly.

Say your driver has three source files: main.c, gpio.c, and irq.c. All three together form one module called mydriver.ko:

# The module is named mydriver
obj-m := mydriver.o

# Tell Kbuild which object files make up this module
# The variable name is: modulename-objs
mydriver-objs := main.o gpio.o irq.o

# Kbuild will:
# 1. Compile main.c → main.o
# 2. Compile gpio.c → gpio.o
# 3. Compile irq.c  → irq.o
# 4. Link all three → mydriver.ko

Important rule: Do not name any of your source files the same as the final module name. If your module is mydriver.ko, do not have a file called mydriver.c (it creates a naming conflict with mydriver.o). Use main.c or mydriver_main.c instead.

Multi-File Module Build Flow
main.c
gpio.c
irq.c
main.o
gpio.o
irq.o
mydriver.ko

🌎 Cross-Compilation — Building for a Different Architecture

In embedded Linux development, you often build kernel modules on a powerful x86 workstation (the host) but run them on an ARM board (the target). This is called cross-compilation.

To cross-compile a kernel module, you need three things:

  1. A cross-compiler toolchain installed on your host (e.g., gcc-arm-linux-gnueabihf for 32-bit ARM, or gcc-aarch64-linux-gnu for 64-bit ARM).
  2. The kernel source or headers compiled/configured for the target architecture.
  3. Two extra make variables: ARCH and CROSS_COMPILE.
# Cross-compile for 32-bit ARM (e.g., Raspberry Pi 2/3 in 32-bit mode):
make ARCH=arm \
     CROSS_COMPILE=arm-linux-gnueabihf- \
     KDIR=/path/to/arm-kernel-headers

# Cross-compile for 64-bit ARM (e.g., Raspberry Pi 4, BeagleBone AI):
make ARCH=arm64 \
     CROSS_COMPILE=aarch64-linux-gnu- \
     KDIR=/path/to/arm64-kernel-headers

# ARCH       = target CPU architecture
# CROSS_COMPILE = prefix of your cross-compiler tools
#                 aarch64-linux-gnu- means the compiler is:
#                 aarch64-linux-gnu-gcc, aarch64-linux-gnu-ld, etc.

You can bake these into your Makefile using conditional assignment so you only have to change one place:

# Cross-compile Makefile example

# Set these to override from command line or environment
ARCH         ?= arm64
CROSS_COMPILE?= aarch64-linux-gnu-
KDIR         ?= /opt/rpi-kernel/linux

obj-m := mydriver.o

all:
	$(MAKE) -C $(KDIR) M=$(PWD) \
	    ARCH=$(ARCH) \
	    CROSS_COMPILE=$(CROSS_COMPILE) \
	    modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

Verifying cross-compilation worked:

# Check what architecture the .ko was built for:
file mydriver.ko
# Output should say "ARM aarch64" not "x86-64"

# Also check:
modinfo mydriver.ko
# vermagic line will show the target kernel's version and architecture

🔧 Custom Compiler Flags with ccflags-y

Sometimes you need to pass extra compiler flags — for example, to add include paths, define macros, or enable/disable specific warnings. In Kbuild, you use ccflags-y for this (not CFLAGS, which is reserved).

# In your Makefile (inside the Kbuild context):

# Add a preprocessor define:
ccflags-y += -DMY_DRIVER_DEBUG

# Add an include directory:
ccflags-y += -I$(src)/include

# Enable extra warnings:
ccflags-y += -Wall -Wextra

# Disable a specific warning:
ccflags-y += -Wno-unused-variable

# Multiple flags:
ccflags-y += -DBOARD_REVISION=2 -DFEATURE_X_ENABLED

Note: Use $(src) (not $(PWD)) for paths inside the Kbuild context — $(src) is the absolute path to your module source directory as set by Kbuild.

📥 Installing Your Module — Proper Integration

During development you typically use insmod ./mymodule.ko directly. When you are ready to deploy your module properly so that modprobe can find it by name, you need to install it.

# Install the module (copies to /lib/modules/$(uname -r)/extra/):
sudo make modules_install

# or manually copy it to any subdirectory under /lib/modules/$(uname -r)/:
sudo cp mydriver.ko /lib/modules/$(uname -r)/extra/

# CRITICAL: Always run depmod after installing a new module!
sudo depmod -a

# Now you can load it by name:
sudo modprobe mydriver

# Verify it is found:
modinfo mydriver   # should work without path

The extra/ subdirectory is the conventional location for out-of-tree modules. The kernel won’t put anything there itself, so there is no conflict with kernel-provided modules.

To make a module load automatically at every boot:

# Create a configuration file for auto-loading:
echo "mydriver" | sudo tee /etc/modules-load.d/mydriver.conf

# If the module needs parameters at load time:
echo "options mydriver my_param=10" | sudo tee /etc/modprobe.d/mydriver.conf

🏆 Chapter 4 Summary — What You Have Learned
Part 1 🔗
User space vs kernel space, CPU privilege rings (x86 and ARM), system call interface, major kernel subsystems, monolithic vs microkernel.
Part 2 🔌
LKM concept, built-in vs module, .ko file format, module lifecycle, init/exit functions, metadata macros, vermagic, modules on disk.
Part 3 📄
Prerequisites, complete Hello World module, header files, __init/__exit, pr_info, Makefile, build-load-test-unload workflow, common errors.
Part 4 🛠
insmod, rmmod, lsmod, modinfo, modprobe, depmod, module parameters, /proc/modules, /sys/module filesystem.
Part 5 📥
printk, all 8 log levels, pr_* macros, pr_fmt, dev_* macros for drivers, kernel ring buffer, dmesg, console log level, dynamic debug.
Part 6 🏗
Kbuild two-pass mechanism, obj-m/obj-y/obj-n, multi-file modules, cross-compilation (ARCH + CROSS_COMPILE), ccflags-y, module installation.

🏆 Interview Questions — Kbuild and Module Building
Q1. Explain the two-pass mechanism of kernel module compilation.
First pass: make is run in your module directory. Your Makefile invokes the kernel’s build system by changing to the kernel build directory (-C KDIR). Second pass: the kernel’s build system re-invokes make in your source directory (M=PWD) from inside the Kbuild environment, where KERNELRELEASE is defined. In this second pass, your obj-m assignments are processed and the .ko is built with the correct compiler flags.
Q2. How do you build a kernel module from multiple source files?
Use the modulename-objs variable. For example: obj-m := mydriver.o and mydriver-objs := main.o gpio.o irq.o. Kbuild will compile each .c file separately and link the resulting .o files into mydriver.ko. Never name a source file the same as the final module name to avoid naming conflicts.
Q3. What is the purpose of ARCH and CROSS_COMPILE in the module Makefile?
ARCH specifies the target CPU architecture (arm, arm64, x86, mips, etc.). CROSS_COMPILE is the prefix of the cross-compiler toolchain (e.g., aarch64-linux-gnu-). When set, the kernel build system uses the cross-compiler instead of the host compiler, producing a .ko file for the target architecture.
Q4. What steps do you take after copying a .ko file to /lib/modules?
Run sudo depmod -a to regenerate the module dependency database (modules.dep, modules.alias, etc.). Without this step, modprobe will not find the new module because it only searches the database built by depmod, not the actual files on disk.
Q5. What is obj-$(CONFIG_MY_FEATURE) and why is it better than hardcoding obj-m?
obj-$(CONFIG_MY_FEATURE) expands to obj-m, obj-y, or obj-n depending on the Kconfig setting for MY_FEATURE. This allows a single Makefile to handle all three cases: compile as a module (m), compile built-in (y), or don’t compile at all (n). It ties the build to the user’s make menuconfig choices, making drivers properly configurable.
Q6. Why should you use ccflags-y instead of CFLAGS in a kernel module Makefile?
CFLAGS is reserved by GNU Make for the user’s use and may conflict with the kernel build system’s own CFLAGS. ccflags-y is the Kbuild-defined variable for adding extra compiler flags to all C files in the current directory. Using it ensures the flags are properly applied within the Kbuild framework without overriding system-level flags.

Chapter 4 Complete! 🎉
You now have a solid foundation in kernel module development. Coming up in Chapter 5: Character Device Drivers — creating /dev entries, file operations, and talking to user space.

← Part 5
EmbeddedPathashala Home →

Previous Lecture
Next Lecture

Leave a Reply

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