Building and Loading Your Own Linux Kernel Module
A minimal but complete walkthrough of writing, building, loading, and communicating with a Linux kernel module.
Most of the Linux kernel’s functionality — device drivers, filesystems, network protocols — can be compiled either directly into the kernel image or as a loadable kernel module (LKM), inserted and removed at runtime without a reboot. Writing a minimal one is the fastest way to understand how kernel code actually differs from userspace code, starting with the fact that there’s no libc, no printf, and no safety net if you get it wrong.
The minimal module
A kernel module needs exactly two functions — one run at load time, one at unload — registered through macros the kernel build system recognizes:
// hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void) {
printk(KERN_INFO "hello: module loaded\n");
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO "hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Daniel Cosenza");
MODULE_DESCRIPTION("A minimal example kernel module");
printk is the kernel’s equivalent of printf — it doesn’t write to a terminal directly, but into the kernel’s ring buffer, readable via dmesg.
Building against the running kernel’s headers
Kernel modules must be compiled against the headers matching the exact running kernel version — a module built for one kernel build generally won’t load on another. The kernel build system provides a Makefile pattern specifically for this, driven by the kernel-headers/kernel-devel package already installed for the running kernel:
# Makefile
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
make
ls hello.ko
Loading, inspecting, and unloading
sudo insmod hello.ko
dmesg | tail
lsmod | grep hello
sudo rmmod hello
dmesg | tail
modinfo inspects a compiled module’s metadata — license, description, parameters — without loading it, which is worth checking before loading anything from an unfamiliar source, since MODULE_LICENSE also affects which kernel symbols a module is allowed to call:
modinfo hello.ko
Module parameters
Kernel modules can accept parameters at load time, declared with module_param and readable/writable afterward through a corresponding entry under /sys/module/:
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of greetings to log");
sudo insmod hello.ko count=3
cat /sys/module/hello/parameters/count
Exposing a device: a minimal character device
Most real modules exist to expose a device to userspace. Registering a character device gives the module a major number and a set of file operations userspace interacts with through ordinary open/read/write calls on a /dev node:
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = my_read,
.write = my_write,
};
static int __init chardev_init(void) {
major = register_chrdev(0, "mydevice", &fops);
return 0;
}
mknod /dev/mydevice c <major> 0
cat /dev/mydevice
This is precisely the mechanism the VFS abstraction relies on — from userspace, /dev/mydevice looks and behaves exactly like any other file, dispatching to the module’s own read/write implementations underneath.
Why kernel code is a different discipline
A crash in userspace kills one process; a crash in a kernel module can panic the entire machine, since kernel code runs with full hardware access and no memory protection from the rest of the kernel. There’s no dynamic memory allocator to lean on carelessly (kmalloc/kfree instead of malloc/free, with explicit, more restrictive allocation flags), no floating point by default, and every blocking operation needs to be reasoned about in terms of what context it’s safe to call from (process context versus interrupt context, in particular). This is also why kernel modules are version- and configuration-sensitive in a way userspace binaries aren’t — they’re linked directly against a specific kernel build’s internal symbol table, not a stable ABI, which is a deliberate trade-off the kernel community has made in favor of being able to freely refactor internal interfaces over time.