04 · Kernel Modules: Loading, Building & Debugging¶
Level 1 described the kernel as monolithic but modular: drivers and subsystems can be compiled separately and loaded or unloaded at runtime without rebooting. This lesson makes that concrete by writing, building, loading, and unloading the smallest possible real kernel module — a handful of lines of C that genuinely runs inside the kernel, not a simulation of one.
A real module, start to finish¶
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void) {
printk(KERN_INFO "hellomod: loaded, says hi from the kernel\n");
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO "hellomod: unloading, bye from the kernel\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
Building and running it for real¶
$ make
make -C /lib/modules/7.0.0-34-generic/build M=/tmp/hellomod modules
warning: the compiler differs from the one used to build the kernel
warning: pahole version differs from the one used to build the kernel
CC [M] hello.o
LD [M] hello.ko
BTF [M] hello.ko
Skipping BTF generation for hello.ko due to unavailability of vmlinux
$ sudo insmod hello.ko
$ lsmod | grep hello
hello 12288 0
$ sudo rmmod hello
$ sudo dmesg | grep hellomod
[22658.129731] hellomod: loaded, says hi from the kernel
[22658.157522] hellomod: unloading, bye from the kernel
(Real output from this course's lab VM — this module genuinely compiled against the running kernel's real
headers, loaded into the live kernel, and printed real kernel log messages visible via dmesg.) The build
emitted two real, honest warnings worth understanding rather than ignoring: the compiler and pahole (a tool
that generates BTF — BPF Type Format — debug metadata) versions differ slightly from what originally built this
distro's shipped kernel, which is common for out-of-tree modules and usually harmless for a module this simple,
but is exactly the kind of mismatch that can cause real incompatibilities for more complex modules depending on
kernel internals that changed between compiler/toolchain versions. lsmod confirms the module is loaded, with a
size and a usage count of 0 (nothing currently depends on it, so rmmod is safe). The two dmesg lines prove
both lifecycle functions — module_init's hello_init and module_exit's hello_exit — genuinely ran inside
the kernel at load and unload time respectively.
How It Actually Works¶
A kernel module is compiled as position-independent relocatable object code targeting the exact running
kernel's internal ABI — its struct layouts, exported symbol addresses, and calling conventions — which is why
/lib/modules/<kernel-version>/build (a path to that specific kernel's build artifacts and headers, installed by
the matching linux-headers package) has to match the actual running kernel precisely; a module built against
one kernel version's headers will very often refuse to load, or worse, load and behave unpredictably, against a
different kernel build. insmod performs the actual load: it reads the .ko file, resolves its symbol
references against the running kernel's exported symbol table, relocates the code into kernel memory, and calls
its init function — at which point the module's code is running with full kernel privileges, sharing the same
address space and fault domain as every other kernel subsystem (tying back to Level 1's point about monolithic
kernel design: a badly-written module can, in principle, crash the whole system, which is exactly why loading
unknown or untrusted modules is a genuine security and stability decision, not a casual one).
modprobe (used far more often than raw insmod in practice) adds dependency resolution on top of this: it
reads modules.dep (generated by depmod) to load any modules the requested one depends on first, and knows
how to find modules by logical name rather than requiring a full .ko path — the same load-order problem APT and
DNF solve for packages (Level 1, Lesson 9), applied to kernel modules specifically.
Common mistakes¶
- Building a module against mismatched kernel headers and being surprised when
insmodrefuses it or the system becomes unstable. Always confirmuname -rmatches the headers package actually used for the build — a version mismatch is the single most common cause of "it compiled fine but won't load." - Treating
rmmodas always safe. A module with a non-zero usage count (visible inlsmod's third column) is actively depended on by something else; forcing its removal anyway (rmmod -f) can crash whatever still held a reference to it. - Writing kernel module code with the same defensive habits as userspace code and assuming equivalent safety. There's no memory protection between a module and the rest of the kernel — a null pointer dereference here doesn't segfault one process, it can take down (or corrupt) the entire system; kernel code is held to a substantially stricter correctness bar for exactly this reason.
Exercise¶
Build this lesson's exact hello.ko module on a Linux machine with the right kernel headers installed
(sudo apt install build-essential linux-headers-$(uname -r) on Debian/Ubuntu, or the RPM-family equivalent),
load it with insmod, confirm its messages in dmesg, then unload it with rmmod and confirm the exit message
too. Then deliberately change uname -r in your mental model only (don't actually switch kernels) and explain,
from this lesson's model, why the exact same .ko file would likely fail to load against a different kernel
build without rebuilding it against that kernel's own headers.