Skip to content

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");
obj-m += hello.o
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

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 insmod refuses it or the system becomes unstable. Always confirm uname -r matches 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 rmmod as always safe. A module with a non-zero usage count (visible in lsmod'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.