obj-m vs obj-y
Multi-file Modules
EXTRA_CFLAGS
Cross Compilation
ARCH and CROSS_COMPILE
Kconfig Symbols
ccflags-y
Install a Module
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.
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:
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.M=$(PWD)). Now it reads your Makefile a second time — but this time from inside the Kbuild environment.obj-m assignments, compiles your source files with the exact same compiler flags used to build the kernel itself, and links the .ko file.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).
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
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.
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.
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:
- A cross-compiler toolchain installed on your host (e.g.,
gcc-arm-linux-gnueabihffor 32-bit ARM, orgcc-aarch64-linux-gnufor 64-bit ARM). - The kernel source or headers compiled/configured for the target architecture.
- Two extra make variables:
ARCHandCROSS_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
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.
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
