Linux Kernel Module Build System: Makefiles, Kbuild, obj-m & obj-y

Kernel Module Makefile & Kbuild
How Linux 6.x builds your kernel module — obj-m, obj-y, and the recursive make system explained
Skill Level
Beginner → Intermediate
Kernel
Linux 6.x
Part
3 of 3
Series: PREV_LEC NEXT_LEC
Topics Covered:
kernel module Makefile Kbuild system linux obj-m obj-y linux kernel module build insmod rmmod free linux kernel programming course free linux device driver course linux 6.x LKM

You have written your first kernel module. You have set up your logging with pr_fmt(). Now — how do you actually compile it into a .ko file that the kernel can load?

The answer is the Kbuild system. It is the build infrastructure that compiles not just your module, but the entire Linux kernel. Understanding how it works — and how to write a correct Makefile for your module — is an essential skill for every kernel developer and Linux device driver author.

1. What Is the Kbuild System?

The Linux kernel does not use a plain handwritten Makefile the way a simple C project might. It uses a layered, recursive build system called Kbuild. Kbuild is responsible for compiling thousands of C files with the exact same compiler flags, enforcing configuration choices made through menuconfig, and producing the final kernel image and any kernel modules.

When you build a kernel module outside the kernel source tree — called an out-of-tree module — you are not working in isolation. Your module’s Makefile delegates the actual compilation work to the Kbuild system. This guarantees that your module is compiled with precisely the same compiler settings, ABI conventions, and header definitions as the running kernel it will be loaded into.

Why does this matter? Kernel modules are not standalone programs. They are binary extensions that get linked directly into the running kernel’s memory space. If your module were compiled with even slightly different flags or headers than the running kernel, loading it could cause undefined behaviour, random crashes, or silent data corruption. Kbuild prevents this by coupling the module build tightly to the kernel it targets.
Kbuild system — two roles it plays
Role 1: Built-in compilation (obj-y)

Compiles kernel subsystems and drivers that are permanently baked into the kernel image (vmlinux). Selected during make menuconfig with Y.
| Role 2: Module compilation (obj-m)

Compiles code as separate .ko files that can be loaded and unloaded at runtime with insmod and rmmod. Selected with M.

2. obj-y and obj-m — The Core Concept

Deep inside the Kbuild system, two special Make variables control what gets built and how:

obj-y — Built into the kernel

Anything added to obj-y gets compiled and linked directly into the kernel image — the vmlinux binary. It is always present whenever that kernel boots. The y stands for Yes, matching the Y you select in menuconfig.

# Example from a driver's internal Kbuild file (not your module's Makefile)
# This compiles i2c-core into the kernel image itself
obj-y += i2c-core.o

obj-m — Built as a loadable module

Anything added to obj-m gets compiled into a separate .ko file. It is not present in memory until someone explicitly loads it with insmod or modprobe. The m stands for Module.

This is the variable you use in your own module’s Makefile:

# Your module's Makefile — tell Kbuild to build your source as a module
obj-m += my_gpio_driver.o

When Kbuild sees this, it looks for a file named my_gpio_driver.c, compiles it into my_gpio_driver.o, and then links it into my_gpio_driver.ko.

Fate of code based on obj-y vs obj-m assignment
Variable menuconfig choice Output file How to load Can unload?
obj-y Y (built-in) vmlinux / bzImage Always in memory at boot No
obj-m M (module) driver_name.ko insmod / modprobe Yes (rmmod)
(not set) N (disabled) Not compiled N/A N/A

3. Anatomy of a Complete Kernel Module Makefile

Here is a complete, production-ready Makefile for an out-of-tree kernel module, written for Linux 6.x:

# Kernel module Makefile for Linux 6.x
# Place this file in the same directory as your .c source file

# Module name — must match your .c filename
MODULE_NAME := my_gpio_driver

# Add module to the Kbuild out-of-tree build list
obj-m += $(MODULE_NAME).o

# Locate the kernel headers for the currently running kernel
KERNEL_DIR := /lib/modules/$(shell uname -r)/build

# Save the current working directory
PWD := $(shell pwd)

# === BUILD TARGETS ===

# Default target — builds the .ko file
all:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

# Install the .ko into the system's module directory
install:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install

# Remove all generated build artifacts
clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

# === OPTIONAL — enable pr_debug() output ===
# Uncomment the line below to activate debug messages
# EXTRA_CFLAGS += -DDEBUG
Makefile formatting rule: Every recipe line (the commands under a target like all:) must be indented with a real Tab character, not spaces. If you use spaces, make will throw a missing separator error and refuse to run. Most text editors can be configured to insert tabs; check your editor’s settings if you see this error.

4. How the Build Process Works Step by Step

When you type make in your module directory, here is exactly what happens:

Step-by-step flow when you type “make” in your module directory
Step 1 — make reads your Makefile and finds the all: target
Step 2 — -C $(KERNEL_DIR) tells make to change directory into the kernel headers tree at /lib/modules/$(uname -r)/build
Step 3 — Kbuild parses the kernel’s top-level Makefile. This loads all compiler flags, ABI settings, and architecture configuration for the running kernel
Step 4 — M=$(PWD) tells Kbuild: “now switch back to the module’s directory and build the modules there”
Step 5 — Kbuild compiles your .c into .o using the kernel’s exact gcc flags, then links it with module glue code into .ko
Step 6 — your my_gpio_driver.ko is ready in the current directory

This recursive structure — where your Makefile hands control to the kernel’s Makefile which then comes back to your directory — is the defining characteristic of out-of-tree module builds. It ensures binary compatibility between your module and the kernel.

5. The Kernel Headers Package

The path /lib/modules/$(uname -r)/build is not the full kernel source tree. It is a much smaller kernel headers package installed by your Linux distribution. It contains:

  • The top-level kernel Makefile (with all compiler flags)
  • Architecture-specific headers and configuration
  • The .config file recording every Kconfig option set during the kernel’s build
  • The Module.symvers file listing all exported kernel symbols and their checksums

It does not include the full kernel C source code. You only need that if you are patching or rebuilding the kernel itself.

# Install kernel headers on Debian / Ubuntu
sudo apt install linux-headers-$(uname -r)

# Install kernel headers on Fedora / RHEL
sudo dnf install kernel-devel-$(uname -r)

# Verify the headers directory exists
ls /lib/modules/$(uname -r)/build

6. Building a Module With Multiple Source Files

Real drivers are rarely a single file. When your module spans multiple .c files, Kbuild provides a clean way to specify all of them. You define the final module name and list the object files that should be linked together to form it:

# Makefile for a multi-file kernel module
# This builds my_spi_driver.ko from three .c files

obj-m += my_spi_driver.o

# Tell Kbuild which .o files make up my_spi_driver.o
my_spi_driver-objs := spi_core.o spi_dma.o spi_irq.o

KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD        := $(shell pwd)

all:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

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

With this setup, Kbuild compiles spi_core.c, spi_dma.c, and spi_irq.c individually into object files, then links them into a single my_spi_driver.o, and finally produces my_spi_driver.ko.

7. Understanding the Generated Build Files

After a successful build, your module directory contains several files besides the .ko:

Files generated in your module directory after running make
File What it is
my_driver.ko The final loadable kernel module — the file you pass to insmod
my_driver.o Compiled object file before module linking stage
my_driver.mod.c Auto-generated C file with module metadata (license, version magic, dependencies)
my_driver.mod.o Compiled version of the auto-generated metadata file
Module.symvers Symbol version checksums — used if your module exports symbols for other modules
modules.order Order in which modules were built — used by modprobe
.my_driver.ko.cmd Hidden file: the exact linker command used to create the .ko (useful for debugging build issues)

Run make clean to remove all generated files except your original .c source and your Makefile.

8. Loading and Unloading Your Module

# Load your module
sudo insmod ./my_gpio_driver.ko

# Verify it loaded — check the kernel log
dmesg | tail -20

# List all currently loaded modules
lsmod | grep my_gpio

# View module information (license, author, description, parameters)
modinfo ./my_gpio_driver.ko

# Unload the module
sudo rmmod my_gpio_driver

# Verify it unloaded
dmesg | tail -5
insmod vs modprobe: insmod loads a module directly from a path you specify. modprobe is smarter — it searches the system’s module directories, automatically resolves dependencies, and loads any required modules first. For development use insmod; for production deployment use modprobe after running make install and depmod -a.

9. Version Magic — Why Your Module May Refuse to Load

Every kernel module compiled by Kbuild gets a vermagic string embedded in it. This string encodes the exact kernel version, SMP support status, preemption model, and other build attributes. When you try to load a module, the kernel checks this string against its own version. If they do not match exactly, the kernel refuses to load the module.

# See the vermagic string embedded in your .ko file
modinfo my_gpio_driver.ko | grep vermagic

# Example output:
# vermagic: 6.8.0-45-generic SMP preempt mod_unload modversions aarch64
Common mistake: Compiling a module against the headers for kernel version 6.8.0-45 and then trying to load it on a system running 6.8.0-47 (after a kernel update). Even a minor version difference causes a vermagic mismatch. Always recompile your module after a kernel update.

Interview Questions & Answers

Q1. What is the difference between obj-y and obj-m in the Kbuild system?
A: obj-y contains a list of object files to compile and link permanently into the kernel image (vmlinux). These subsystems or drivers are always present in memory. obj-m contains object files to compile as separate loadable kernel modules (.ko files) that can be inserted and removed at runtime. The letters match the Kconfig selection: Y for built-in, M for module.
Q2. Why does the kernel module Makefile invoke make with the -C flag pointing to the kernel headers directory?
A: The -C flag causes make to change into the kernel headers directory first and parse the kernel’s own top-level Makefile. This loads all the exact compiler flags, architecture settings, ABI conventions, and exported symbol checksums that were used when the running kernel was compiled. Once those are loaded, Kbuild switches back to the module directory (via the M= variable) to compile the module. This guarantees binary compatibility between the module and the running kernel.
Q3. What is vermagic and why does it cause module load failures after a kernel update?
A: Vermagic is a string embedded in every .ko file that encodes the exact kernel version, SMP configuration, preemption model, and build flags. When loading a module, the kernel compares this string against its own. A module compiled for kernel 6.8.0-45 will be rejected on 6.8.0-47 because the vermagic strings differ. The solution is to recompile the module against the new kernel headers each time the kernel is updated.
Q4. How do you build a kernel module that spans multiple .c source files?
A: You use a compound object assignment in the Makefile. First you add the final module name to obj-m. Then you use a separate variable named modulename-objs to list all the individual object files that should be linked together. Kbuild compiles each .c file independently, links the resulting .o files into a single combined object, and produces one final .ko file.
Q5. What is the difference between insmod and modprobe? Which should you use in production?
A: insmod loads a module directly from a file path you specify but does not handle dependencies — if your module depends on another module that is not yet loaded, insmod will fail. modprobe reads the dependency database generated by depmod and automatically loads any required modules first. For development, insmod is convenient. For production deployment, modprobe is correct and should be used after installing the module with make install and rebuilding the module dependency database with depmod -a.
Q6. What is the kernel headers package and why is the full kernel source not needed to build an out-of-tree module?
A: The kernel headers package is a small subset of the kernel source tree — the Makefile infrastructure, header files, configuration, and symbol version tables — sufficient for compiling external modules against a specific running kernel. The full kernel source (several gigabytes) also contains all driver implementations, filesystem code, networking stack, and so on, which are not needed to compile an external module. Distributions ship the headers package as a separate installable package specifically to enable out-of-tree module development without requiring the full source.

Free Linux Kernel & Device Driver Course

You have completed the printk and Kbuild trilogy. Continue with the full EmbeddedPathashala Linux kernel programming course.

← Start from Part 1 ← Part 2: Format Specifiers
← Previous EmbeddedPathashala — Free Linux Kernel & Device Driver Course

Leave a Reply

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