Building Multi-File Linux Kernel Modules with Kbuild

Building Multi-File Linux Kernel Modules with Kbuild Makefiles | EmbeddedPathashala

Free Linux Kernel Programming Course › LKM Series › Module 4

Building Multi-File Linux Kernel Modules with Kbuild

How the Linux 6.x kernel build system links multiple C source files into a single loadable kernel module — explained from the ground up.

Linux 6.x Kbuild / Makefile Kernel Modules Free Course Beginner Friendly

1. Why Would You Split a Kernel Module Across Multiple Files?

When you are writing a small learning exercise, having everything in a single .c file is perfectly fine. But real-world kernel modules — device drivers, file systems, protocol implementations — tend to grow quickly. Cramming thousands of lines into one file makes the code hard to read, hard to test, and a nightmare to maintain.

The same reasons that push userspace programmers toward breaking code into separate files apply here too. You might want one file that handles hardware register access, another for interrupt handling, and a third for the sysfs interface. Each file stays focused, and the team can work on different pieces in parallel without stomping on each other’s changes.

The good news is that the Linux kernel build system — called Kbuild — handles this pattern natively. You do not need any special linker scripts or complex build logic. A few lines in a Makefile are all it takes.

Key Term Kbuild is the kernel’s own build infrastructure. When you run make -C /lib/modules/$(uname -r)/build M=$(pwd) modules, it is Kbuild that reads your Makefile, compiles your source files with the correct kernel headers and compiler flags, and produces the final .ko file.

2. How the Kbuild System Processes a Makefile

When you build an out-of-tree kernel module, there are actually two Makefiles involved. Your module directory has a Makefile written for Kbuild. When you invoke make with the kernel source tree as the build root, the kernel’s own top-level Makefile orchestrates the build and then hands control to your Makefile for the module-specific rules.

How Kbuild Processes Your Module Makefile

Your module Makefile’s job is straightforward: tell Kbuild which source files belong to your module and what the final module file should be called. Kbuild then takes care of finding the right compiler, applying the right flags for your running kernel version, and linking everything correctly.

In Linux 6.x, this mechanism has been stable for many years. The obj-m variable is the key entry point that tells Kbuild your module should be built as a loadable module (as opposed to being compiled into the kernel image itself).

3. The obj-m Directive and the -objs Trick

For a single-file module, a Makefile is very short. You just tell Kbuild the name of the module object:

# Single-file module example
# This builds hello.c into hello.ko

obj-m := hello.o

Kbuild sees hello.o listed under obj-m, looks for hello.c, compiles it, and produces hello.ko. Simple.

Now, what if your module is split across multiple .c files? This is where the -objs convention comes in. The pattern is:

  1. Give your module a label (e.g., projx).
  2. Assign that label to obj-m with a .o suffix.
  3. Use the same label with a -objs suffix to list all the source object files that should be compiled and then linked together.
# Multi-file kernel module Makefile
# Final module will be named projx.ko
# Source files: prj1.c, prj2.c, prj3.c

obj-m := projx.o
projx-objs := prj1.o prj2.o prj3.o

Kbuild reads this and understands: compile prj1.c, prj2.c, and prj3.c separately into their own .o object files, then link all three into a single binary kernel module called projx.ko.

Quick Rule The label you choose (here: projx) must be the same on both lines. The obj-m line uses it with .o, and the -objs line uses it as a prefix. The label itself can be anything you want — it is just a name you invent.
Multi-File Module: Compile Then Link

4. What Happens Step by Step During the Build

It helps to understand what Kbuild actually does behind the scenes when you run the build command. Here is the sequence:

Step-by-Step Kbuild Build Sequence

After step 4, you have a projx.ko file that you can load with insmod projx.ko or modprobe projx. The .ko file is not a standard ELF executable — it is a relocatable object that the kernel’s module loader maps into kernel address space at load time.

Linux 6.x Note In recent kernels (6.4 and later), the .ko format gained support for BTF (BPF Type Format) sections embedded inside the module. These sections help tools like bpftool and pahole understand your module’s data types. Kbuild adds BTF automatically if your toolchain and kernel config support it — you do not need to do anything special.

5. Sharing Code Between Modules — the Kernel Library Pattern

The same multi-file linking mechanism gives you a clean way to share utility code across different kernel modules without the complexity of exporting symbols. The idea is simple: you maintain a set of helper .c files that any module in your project can link into itself at build time.

Think of it as a private kernel-side library. The library code is compiled and linked directly into each module that needs it, rather than being a separate loadable module on its own.

Here is how this looks in a real Makefile. Suppose you have a helper file called kutil.c sitting one directory up, and your module drv_core.c needs the functions inside it:

# Makefile for drv_core module
# Links in shared utility code from the parent directory

obj-m += drv_core_full.o
drv_core_full-objs := drv_core.o ../kutil.o

Kbuild will compile both drv_core.c and ../kutil.c, then link the resulting object files into a single module called drv_core_full.ko. The utility functions in kutil.c become part of your module binary.

Kernel Library Pattern — Linking Helper Code Into Multiple Modules
Trade-off to Keep in Mind With this pattern, the utility code gets compiled into each module separately. That means each .ko file carries its own copy of those helper functions. If you have many modules all linking the same large utility file, you end up with duplicated code in memory when they are all loaded. For a few small helper functions this is perfectly fine. When the shared code grows large, using EXPORT_SYMBOL() and a dedicated module is the better approach — which is exactly what the next lecture covers.

6. Putting It All Together — a Practical Makefile

Here is a complete, annotated Makefile you can use as a starting point for any multi-file out-of-tree kernel module on Linux 6.x. Read each comment carefully — they explain what every line does.

# Kbuild Makefile for a multi-file out-of-tree kernel module
# Works with Linux 6.x
#
# Usage from your module directory:
#   make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
#   make -C /lib/modules/$(uname -r)/build M=$(pwd) clean

# --- Tell Kbuild we want a loadable module called mydrv.ko ---
obj-m := mydrv.o

# --- List all .c source files that make up this module ---
# Kbuild compiles each into a .o and then links them together
mydrv-objs := mydrv_main.o mydrv_hw.o mydrv_sysfs.o

# --- Optional: pass extra compiler flags only for this module ---
# -DDEBUG enables debug printk() calls inside the module
ccflags-y += -DDEBUG -Wall

# --- The two standard make targets (handled by the kernel build system) ---
all:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Modern Tip for Linux 6.x In Linux 6.x, the recommended way to supply per-module compiler flags is with the ccflags-y variable (or the newer CFLAGS_<filename>.o per-file variant). Avoid the older EXTRA_CFLAGS variable — it still works for now but is considered legacy.

Cross-Compiling for ARM Targets

When you are building a kernel module for an embedded ARM board running Linux 6.x, you need to point Kbuild at the correct kernel source tree for your target and tell it which cross-compiler to use. The module Makefile itself does not change — only the command you invoke changes:

# Cross-compile for ARM 64-bit (aarch64) target
# Adjust CROSS_COMPILE and KERNEL_DIR for your setup

export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
KERNEL_DIR=/path/to/arm64/kernel/source

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

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

The ARCH and CROSS_COMPILE environment variables tell the kernel build system which architecture you are targeting and which toolchain prefix to use. The rest of the build proceeds exactly as on a native x86 machine — Kbuild handles all the architecture-specific details internally.

7. Interview Questions

What does the obj-m variable tell the Kbuild system?
It tells Kbuild that the listed object file (and its corresponding source) should be built as a loadable kernel module (.ko) rather than compiled into the vmlinux kernel image. The alternative, obj-y, compiles code directly into the kernel image.
How do you combine three C source files into a single .ko module?
Use the -objs directive in your Kbuild Makefile. Set obj-m := mymodule.o and then mymodule-objs := file1.o file2.o file3.o. Kbuild compiles each .c file separately and links the resulting object files into mymodule.ko.
What is the difference between obj-m and obj-y?
obj-m builds the code as a loadable module that can be inserted and removed at runtime using insmod/rmmod. obj-y compiles the code permanently into the kernel image — it is always present and cannot be unloaded. Device drivers that are needed at boot (like the root filesystem driver) typically use obj-y.
Why would you link a shared utility file directly into a module instead of using EXPORT_SYMBOL()?
When the shared code is small and you only have a couple of modules using it, direct linking is simpler — no symbol exports, no separate module to load and manage. For large shared code used by many modules, EXPORT_SYMBOL() is better because it avoids duplicating the code in every module’s binary and in kernel memory.
How does cross-compilation of a kernel module differ from native compilation?
The module Makefile itself is identical. What changes is the build command: you set the ARCH environment variable to the target architecture (e.g., arm64), set CROSS_COMPILE to your toolchain prefix (e.g., aarch64-linux-gnu-), and point -C at the kernel source tree built for your target, not the host machine’s kernel.
What file extension does a loadable kernel module have, and why is it different from a regular shared library?
A kernel module uses the .ko (kernel object) extension. Unlike a userspace shared library (.so), a .ko file is a relocatable ELF object that is loaded directly into kernel address space by the kernel’s module loader. It does not go through the dynamic linker; instead, the kernel itself resolves its symbol references against the exported kernel symbol table.

Part of the Free Linux Kernel Programming Course at EmbeddedPathashala

Free Linux device drivers course • Free embedded systems course • No registration required

Leave a Reply

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