Kernel Modules (LKM) Lifecycle
Kernel modules are plug-in extensions for the Linux kernel — this module teaches you their complete lifecycle, from writing and loading to symbol sharing and signed verification.
Overview
Linux Kernel Modules (LKMs) are one of the most elegant engineering solutions in operating systems design: they let you extend the kernel at runtime without rebooting, without recompiling the entire kernel, and — critically — without risking the entire system if your code has a bug (well, you still crash the kernel, but at least you didn't have to rebuild it from scratch first). A module is a piece of kernel-space object code that can be loaded into a running kernel, linked against the kernel's symbol table, and then unloaded when no longer needed.
Think of the kernel as a city's electrical grid. The grid is always on, always running. A kernel module is like a new appliance you plug in: the moment you connect it, it starts drawing power (using kernel resources), doing its job, and when you unplug it, the grid keeps humming. The appliance must speak the grid's protocol (kernel ABI), must not short-circuit anything, and must clean up after itself when unplugged.
This module covers the complete lifecycle of an LKM, from the moment you write module_init() to the moment the kernel hands back the memory. You will understand how the kernel links your code at load time, how symbols are shared between modules, how module parameters let you configure behavior without recompiling, how the /sys/module filesystem exposes module state to userspace, and why module signing exists and how it works. These are not just academic concepts: every device driver, every filesystem, every network protocol in a Linux system (unless built-in) lives and dies by these mechanisms.
By the end of this module, you will be writing real kernel modules, loading and unloading them, exporting symbols from one module to another, and understanding exactly what happens at each step. This is the gateway to everything else in kernel development: once you can write and load a module, the entire kernel becomes a sandbox for your experiments.
Understanding the LKM lifecycle is the practical foundation for kernel contribution. Almost every driver, filesystem, and protocol subsystem is written as a module or can be built as one. When you read a real kernel driver (say, drivers/net/ethernet/intel/e1000/e1000_main.c), the first things you encounter are module_init, module_exit, MODULE_LICENSE, and EXPORT_SYMBOL. Without understanding these, the code is opaque. With this understanding, the structure of any kernel subsystem becomes immediately readable.
Beyond readability, the LKM model is the standard pathway for kernel contribution at the beginner level. The kernel community actively encourages new contributors to start with out-of-tree modules before submitting patches upstream. Writing a module means you get rapid feedback (load it, test it, crash it, fix it, repeat) without the slow kernel build-and-boot cycle. Many first kernel patches are driver improvements, new hardware IDs, or small subsystem additions — all of which live in the module infrastructure. Mastering the lifecycle means mastering the workflow of real kernel development.
Kernel Module Lifecycle: From Source to Running to Removed
The complete lifecycle of a kernel module: compilation produces an ELF .ko file; insmod/modprobe triggers kernel-side validation, symbol resolution, and relocation; module_init runs and on success the module becomes LIVE with sysfs entries and refcounting; rmmod triggers GOING state, calls module_exit, and frees all resources.
The concepts
01What is a Kernel Module and Why Does It Exist?
A Loadable Kernel Module (LKM) is a compiled object file (.ko) that can be inserted into a running kernel image at runtime. It runs in kernel space — the same privilege level as the core kernel — with full access to kernel data structures and hardware.
The alternative is a monolithic build-in: compile your driver directly into the kernel image (vmlinuz). This works, but has real costs:
- Every new driver requires a full kernel rebuild and reboot.
- The kernel image grows with every feature, even ones you never use.
- Development iteration is painfully slow.
Modules solve all three problems. But they introduce their own complexity: the module must be ABI-compatible with the running kernel, its symbols must be resolved at load time, and it must carefully manage its own resources.
A .ko file is a standard ELF relocatable object, similar to a .o from a userspace gcc -c compile, but with kernel-specific sections: .init.text (initialization code), .exit.text (cleanup code), __ksymtab (exported symbols), __versions (version magic), and others. The insmod / modprobe tool hands this file to the init_module() syscall, and the kernel links it into the live kernel address space.
# See the ELF sections of a compiled module
readelf -S hello.ko | grep -E 'Name|init|exit|ksymtab|versions'
# See the version magic embedded in the module
modinfo hello.ko
02module_init and module_exit: The Entry and Exit Points
Every kernel module must define exactly two hook points: an init function called when the module is loaded, and an exit function called when it is removed.
static int __init my_init(void) { ... }
static void __exit my_exit(void) { ... }
module_init(my_init);
module_exit(my_exit);
The module_init(fn) macro does not call fn. It places a pointer to fn in a special ELF section called .initcall (for built-ins) or records it in the module's metadata (for LKMs). When the kernel loads your module, it finds this pointer and calls it.
The __init annotation tells the compiler to place the function in .init.text. For built-in drivers (not modules), this section is freed after boot — saving memory. For modules, the distinction is less critical but the annotation is still conventional and correct.
The __exit annotation places the function in .exit.text. For built-ins that cannot be unloaded, the linker discards this section entirely, saving space.
Return value of init: return 0 for success, or a negative errno (e.g., -ENOMEM, -ENODEV) for failure. If init returns non-zero, the kernel aborts loading and calls no further setup. The exit function is NOT called on init failure — you must clean up inside init before returning the error.
This cleanup-on-failure pattern is critical and a common source of kernel bugs. The correct pattern uses goto labels for unwinding:
static int __init my_init(void)
{
int ret;
ret = register_thing_a();
if (ret) return ret;
ret = register_thing_b();
if (ret) goto undo_a;
ret = register_thing_c();
if (ret) goto undo_b;
return 0;
undo_b:
unregister_thing_b();
undo_a:
unregister_thing_a();
return ret;
}
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
pr_info("Hello, kernel world!\n");
return 0; /* 0 = success */
}
static void __exit hello_exit(void)
{
pr_info("Goodbye, kernel world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("You");
MODULE_DESCRIPTION("Minimal hello-world kernel module");
03insmod, rmmod, and modprobe: Loading and Unloading
There are three userspace commands for module management. Understanding their differences is important:
insmod module.ko — the lowest-level loader. It reads the .ko file, invokes the init_module(2) (or finit_module(2)) syscall, and the kernel does the rest: validates the module, resolves symbols, relocates addresses, and calls your module_init function. insmod does not resolve dependencies — if your module needs symbols from another module that isn't loaded, insmod fails.
rmmod module_name — invokes delete_module(2). The kernel checks the module's reference count: if any code (another module, an open file handle to a device, etc.) is still using the module, rmmod fails with EBUSY. If the count is zero, the kernel calls your module_exit function, then frees the module's memory and removes it from the module list.
modprobe module_name — the smart loader. It reads /lib/modules/$(uname -r)/modules.dep (generated by depmod) to find all dependencies, loads them in the correct order, then loads your module. modprobe -r module_name does the reverse: unloads the module and any dependencies that are no longer needed. This is what init systems, udev, and the kernel's request_module() call use in practice.
What the kernel does during insmod:
- Copies the ELF
.kofile into kernel memory. - Verifies the
vermagicstring (kernel version + compiler flags + SMP/preempt config) embedded in the module matches the running kernel. - Resolves undefined symbols against the kernel's exported symbol table (
/proc/kallsyms). - Applies ELF relocations to fix up addresses.
- Runs module notifiers.
- Calls your
module_initfunction. - On success, adds the module to the
moduleslinked list in the kernel.
You can observe the modules list at /proc/modules or with lsmod:
Module Size Used by
e1000 143360 0
crc32_generic 16384 1 e1000
The three columns are: module name, size in bytes, use count (and which modules use it).
# Load a module with full path (insmod, no dep resolution)
sudo insmod ./hello.ko
# Check it loaded
lsmod | grep hello
dmesg | tail -5
# Inspect module metadata before loading
modinfo ./hello.ko
# Load with automatic dependency resolution
sudo modprobe e1000
# Unload
sudo rmmod hello
# or
sudo modprobe -r e1000
# See module dependencies
cat /lib/modules/$(uname -r)/modules.dep | grep e1000
04MODULE_LICENSE and the GPL Symbol Boundary
MODULE_LICENSE("GPL") is not just a comment or bureaucratic formality — it has real, enforced consequences.
The Linux kernel is licensed under GPLv2. Many kernel symbols are deliberately exported only to GPL-licensed modules using EXPORT_SYMBOL_GPL(). If your module does not declare MODULE_LICENSE("GPL") (or a compatible license like "GPL v2" or "Dual MIT/GPL"), the kernel will:
- Taint the kernel (set the
TAINTEDflag, visible in/proc/sys/kernel/tainted). - Refuse to resolve
EXPORT_SYMBOL_GPL-exported symbols for your module, causing load failure.
This is a policy mechanism: it prevents proprietary drivers from using internal kernel APIs that are only intended for open-source code. Nvidia's proprietary driver, for example, wraps kernel functionality in a thin GPL shim precisely because of this boundary.
Common valid license strings:
"GPL"— GNU General Public License v2 (most common)"GPL v2"— explicitly v2"GPL and additional rights""Dual BSD/GPL","Dual MIT/GPL"— dual licensed"Proprietary"— taints the kernel, no GPL symbols
Tainting the kernel is significant: when you file a bug report with a tainted kernel, maintainers will (correctly) ask you to reproduce without the tainted module. Taint flags are documented in Documentation/admin-guide/tainted-kernels.rst.
MODULE_LICENSE("GPL"); /* Full GPL, accesses EXPORT_SYMBOL_GPL */
MODULE_LICENSE("Dual MIT/GPL"); /* Also gets GPL symbol access */
MODULE_LICENSE("Proprietary"); /* Taints kernel, no GPL symbols */
/* In the kernel source, a GPL-only symbol looks like: */
EXPORT_SYMBOL_GPL(some_internal_function); /* GPL modules only */
EXPORT_SYMBOL(some_public_function); /* Any module */
05EXPORT_SYMBOL and the Kernel Symbol Table
When module A needs to call a function defined in module B (or in the core kernel), those symbols must be exported — made visible in the global symbol table that the linker searches at module load time.
EXPORT_SYMBOL(name) — exports a symbol to any module, regardless of license.
EXPORT_SYMBOL_GPL(name) — exports a symbol only to modules declaring a GPL-compatible license.
EXPORT_SYMBOL_GPL_FUTURE(name) — deprecated, do not use.
When you call EXPORT_SYMBOL(my_func), the compiler places an entry in the __ksymtab section of the module (or built-in kernel). Each entry contains:
- The symbol's address (after relocation)
- A CRC of the symbol's type (for version checking)
- The name string
At load time, when module B has an undefined reference to my_func, the kernel searches:
- The core kernel's exported symbol table (from
vmlinux) - All currently-loaded modules' exported symbol tables
If found, the kernel patches module B's address reference. If not found, loading fails with Unknown symbol in module.
You can inspect all exported symbols:
cat /proc/kallsyms | grep ' T ' # T = text (function)
cat /proc/kallsyms | grep my_func
Important rule: you can only export symbols that have external linkage — static functions are NOT exportable (and would be a bad idea: static means "private to this translation unit"). Remove static from any function you want to export.
/* module_a.c — exports a function */
#include <linux/module.h>
#include <linux/kernel.h>
int my_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add); /* available to any module */
int my_secret_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL_GPL(my_secret_add); /* GPL modules only */
static int __init mod_a_init(void) { return 0; }
static void __exit mod_a_exit(void) {}
module_init(mod_a_init);
module_exit(mod_a_exit);
MODULE_LICENSE("GPL");
/* ---------------------------------------------- */
/* module_b.c — uses module_a's exported function */
#include <linux/module.h>
extern int my_add(int a, int b); /* declaration */
static int __init mod_b_init(void)
{
pr_info("3 + 4 = %d\n", my_add(3, 4));
return 0;
}
static void __exit mod_b_exit(void) {}
module_init(mod_b_init);
module_exit(mod_b_exit);
MODULE_LICENSE("GPL");
06Module Parameters with module_param
Rather than hardcoding configuration values, kernel modules can accept parameters passed at load time (via insmod/modprobe) or even changed at runtime via sysfs.
The module_param(name, type, perm) macro declares a module parameter:
name: a variable name in your module (must already be declared).type:bool,int,uint,long,ulong,charp(char pointer),short,ushort.perm: the sysfs file permission for this parameter (e.g.,0644for read-write,0444for read-only,0to not create a sysfs file).
For arrays: module_param_array(name, type, nump, perm) where nump is a pointer to a variable that will hold the actual count of values provided.
Parameters are exposed under /sys/module/<modname>/parameters/<param_name>. If perm is non-zero, you can cat or echo to this file while the module is loaded.
Security note: be careful with charp parameters — the string is a kernel-space pointer to a userspace-provided string; never pass it to functions that assume a fixed-size buffer without bounds checking.
MODULE_PARM_DESC(name, description) provides a human-readable description visible in modinfo.
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
static int repeat = 1;
static char *greeting = "Hello";
static int counts[4];
static int ncounts;
module_param(repeat, int, 0644);
MODULE_PARM_DESC(repeat, "How many times to print the greeting (default: 1)");
module_param(greeting, charp, 0444);
MODULE_PARM_DESC(greeting, "Greeting string to print");
module_param_array(counts, int, &ncounts, 0444);
MODULE_PARM_DESC(counts, "Array of counts");
static int __init param_init(void)
{
int i;
for (i = 0; i < repeat; i++)
pr_info("%s, kernel!\n", greeting);
for (i = 0; i < ncounts; i++)
pr_info("counts[%d] = %d\n", i, counts[i]);
return 0;
}
static void __exit param_exit(void) {}
module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");
/* Load with: sudo insmod param.ko repeat=3 greeting="Hi" counts=1,2,3 */
07Module Dependencies and depmod
When module B uses a symbol exported by module A, B depends on A. Module A must be loaded before module B, and A cannot be unloaded while B is loaded.
The kernel tracks this at runtime: each module has a list_head of dependencies. When you load B, the kernel increments A's reference count. When you try to unload A while B is loaded, rmmod fails with EBUSY.
depmod is the userspace tool that pre-computes all module dependencies by scanning the .ko files in /lib/modules/$(uname -r)/. It writes the result to modules.dep (human-readable) and modules.dep.bin (binary, faster to parse). This is run automatically during kernel installation.
modprobe reads modules.dep to load dependencies in the correct order. It also reads:
modules.alias— maps device IDs (PCI IDs, USB IDs, etc.) to module names, used by udevmodules.builtin— lists modules that are built into the kernel (modprobe skips loading these)
Soft vs. hard dependencies: MODULE_SOFTDEP("pre: module_a") declares a soft dependency — modprobe will try to load module_a first, but won't fail if it's unavailable. This is used for optional features like crypto algorithms.
You can see a module's dependencies:
modprobe --show-depends e1000
# or
cat /lib/modules/$(uname -r)/modules.dep | grep e1000
/* In module_b.c, explicitly declare the dependency: */
MODULE_SOFTDEP("pre: module_a");
/* After building both modules, run: */
/* sudo depmod -a (or just: sudo depmod) */
/* Then modprobe handles everything: */
/* sudo modprobe module_b (auto-loads module_a first) */
/* See the dependency graph: */
/* modprobe --show-depends module_b */
08The /sys/module Filesystem
Every loaded module gets a directory under /sys/module/<module_name>/. This is part of the sysfs virtual filesystem (mounted at /sys), which exposes kernel objects as a directory hierarchy.
For a module named hello, the tree looks like:
/sys/module/hello/
├── initstate # "live" when loaded, "going" during removal
├── refcnt # current reference count
├── srcversion # hash of source at compile time
├── taint # taint flags contributed by this module
├── uevent # write to trigger uevent
├── parameters/ # one file per module_param with non-zero perm
│ ├── repeat
│ └── greeting
└── sections/ # ELF section addresses (CONFIG_KALLSYMS)
├── .text
├── .data
└── .bss
The parameters/ subdirectory is particularly useful: reading a parameter file gives the current value; writing to it (if perm allows) changes the value live, without reloading the module. This is how you tune a running driver without downtime.
# Read a live parameter
cat /sys/module/e1000/parameters/TxDescriptors
# Change it (if writable)
echo 512 | sudo tee /sys/module/e1000/parameters/TxDescriptors
The initstate file is useful for scripts that need to wait until a module is fully initialized: it reads as "live" once module_init returns successfully.
The kernel infrastructure behind sysfs module attributes is in include/linux/module.h and kernel/module.c.
# Explore a loaded module's sysfs tree
ls -la /sys/module/usbcore/
cat /sys/module/usbcore/refcnt
ls /sys/module/usbcore/parameters/
# See all parameters of a module
for f in /sys/module/e1000/parameters/*; do
echo "$(basename $f) = $(cat $f)"
done
# Check kernel taint status
cat /proc/sys/kernel/tainted
# 0 = clean, non-zero = tainted (see Documentation/admin-guide/tainted-kernels.rst)
09Module Signing: Security at Load Time
Module signing is a security mechanism that prevents unauthorized kernel modules from being loaded. It is essential for Secure Boot environments and kernel integrity.
The problem it solves: an attacker with root access can load a rootkit as a kernel module. Module signing ensures that only modules signed with a trusted key can be loaded, even by root.
How it works:
- At build time, a key pair (private + public) is generated. The public key is embedded in the kernel image.
- The
scripts/sign-filetool signs each.kofile: it computes a cryptographic hash of the module's content and encrypts it with the private key. The signature is appended to the.kofile (after a magic~Module signature appended~marker). - At load time, the kernel extracts the signature, verifies it using the embedded public key, and accepts or rejects the module.
Kernel config options controlling signing behavior:
CONFIG_MODULE_SIG— enable module signature checkingCONFIG_MODULE_SIG_FORCE— reject unsigned/incorrectly-signed modules (strict mode)CONFIG_MODULE_SIG_KEY— path to the signing key (default:certs/signing_key.pem)
With CONFIG_MODULE_SIG_FORCE disabled, an unsigned module can still be loaded but it taints the kernel with the E flag (unsigned module).
MOK (Machine Owner Key): on systems with Secure Boot (UEFI), the bootloader only trusts keys in the MOK database. You can enroll your own key with mokutil --import to sign your out-of-tree modules.
Verifying a module's signature:
modinfo --field=sig_id my_module.ko
modinfo --field=sig_hashalgo my_module.ko
The signature data structure is defined in include/linux/module_signature.h.
# Generate a self-signed key pair (for out-of-tree module signing)
openssl req -new -x509 -newkey rsa:2048 -keyout signing_key.pem \
-out signing_cert.pem -days 36500 -subj "/CN=My Module Signing Key/" \
-nodes
# Combine into a single PEM file (what sign-file expects)
cat signing_key.pem signing_cert.pem > mykey.pem
# Sign the module
/usr/src/linux-headers-$(uname -r)/scripts/sign-file sha256 \
signing_key.pem signing_cert.pem hello.ko
# Verify the signature was appended
tail -c 28 hello.ko | strings # should see: ~Module signature appended~
modinfo hello.ko | grep sig
# To enroll your cert for Secure Boot (requires reboot to confirm in UEFI):
# sudo mokutil --import signing_cert.pem
10Building a Module: The Kbuild System
Kernel modules are built using the kernel's Kbuild build system, not just plain gcc. This is necessary because the kernel has specific compile flags, include paths, and symbol definitions that must match exactly.
The minimal Makefile for an out-of-tree module:
obj-m += hello.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
What this does:
obj-m += hello.otells Kbuild to buildhello.cas a module (the-mmeans module;-ywould mean built-in).$(MAKE) -C $(KDIR)changes directory to the kernel build tree and runsmakethere.M=$(PWD)tells the kernel Makefile to look for module source in the current directory.- The kernel's top-level
Makefilethen compiles your module with all the right flags.
For a module with multiple source files:
obj-m += mydriver.o
mydriver-objs := main.o helper.o irq.o
After make, you get:
hello.ko— the loadable modulehello.o— the compiled object before linkingModule.symvers— exported symbol versions from this module (important for inter-module dependencies)modules.order— build order
Module.symvers is critical: when building module B that depends on module A, you must copy (or reference) module A's Module.symvers into module B's build directory so the version checksums match.
# Minimal Makefile for a single-file module
obj-m += hello.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# Build it:
# make
# sudo insmod hello.ko
# dmesg | tail
# sudo rmmod hello
# For multi-file module:
# obj-m += mydriver.o
# mydriver-objs := main.o helper.o
11Module Reference Counting and Safe Unloading
The kernel tracks how many things are currently "using" a module through a reference count. A module cannot be unloaded while its reference count is greater than zero.
The reference count is incremented when:
- Another module depends on this one and is loaded.
- A file operation on a device this module registered is in progress.
- Any kernel mechanism explicitly calls
try_module_get(THIS_MODULE).
Decrements happen via module_put(THIS_MODULE).
In driver code, the kernel handles this automatically for most frameworks: when a character device is opened, the VFS increments the owning module's refcount; when it is closed, the refcount is decremented. You see this as the owner field in structures like struct file_operations:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
};
Setting .owner = THIS_MODULE tells the VFS to call try_module_get() on open and module_put() on close — you get safe refcounting for free.
try_module_get(module): atomically increments the refcount if the module is not in the process of being unloaded (i.e., not in MODULE_STATE_GOING). Returns true on success. Always use try_module_get, never directly increment — it has the race-free state check built in.
A module in state MODULE_STATE_GOING (exit function running) cannot acquire new references, which prevents a race where someone opens a device file at the exact moment you're in module_exit.
# See reference count from userspace
cat /sys/module/hello/refcnt
lsmod | grep hello # third column is refcount
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static struct cdev my_cdev;
static int my_open(struct inode *inode, struct file *filp)
{
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
pr_info("Device closed\n");
return 0;
}
/* .owner = THIS_MODULE gives us automatic refcounting */
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
};
/* While a process has the device open, rmmod will fail with EBUSY */
Key insights
- ✦A .ko file is a standard ELF relocatable object — the kernel is essentially a runtime linker that patches your module's addresses against its live symbol table at load time.
- ✦module_init returning non-zero aborts loading but does NOT call module_exit — you must undo any partial setup yourself, typically using goto-based unwind labels.
- ✦MODULE_LICENSE is not just metadata: it gates access to EXPORT_SYMBOL_GPL symbols, and any non-GPL module taints the kernel — this has real consequences for bug reporting and kernel integrity.
- ✦modprobe is almost always the right tool: it handles dependency loading/unloading automatically via the depmod-generated modules.dep file; insmod is for direct, dependency-free loading.
- ✦Module parameters exposed via /sys/module/<name>/parameters/ can be changed at runtime without reloading the module — this is the kernel's live configuration mechanism for drivers.
- ✦Setting .owner = THIS_MODULE in a file_operations struct gives you race-free reference counting for free: the VFS handles try_module_get/module_put on open/close.
- ✦Module signing + Secure Boot creates a cryptographic chain of trust from firmware through bootloader to kernel modules — understanding this is essential for any production kernel deployment.
- ✦The version magic string embedded in every .ko must match the running kernel exactly: a mismatch means your module was built for a different kernel and could corrupt data structures.
Common pitfalls
- ⚠Forgetting to unwind partial initialization in module_init on failure — if register_thing_a() succeeds but register_thing_b() fails and you return an error without calling unregister_thing_a(), you leak kernel resources that persist until reboot.
- ⚠Using EXPORT_SYMBOL_GPL symbols in a module declared MODULE_LICENSE("Proprietary") — this causes a hard load failure with 'Unknown symbol', not just a warning, and confuses developers who don't know about the GPL boundary.
- ⚠Building a module against one kernel version and loading it on a different version — the vermagic mismatch causes insmod to fail; always build against the headers matching the running kernel (uname -r).
- ⚠Forgetting to set .owner = THIS_MODULE in file_operations — this creates a race: a process can have your device file open while you rmmod the module, leading to a null pointer dereference when it calls your file operation.
- ⚠Calling module_exit logic from inside module_init error paths — module_exit is only called during rmmod, never during failed init; manual cleanup inside init is mandatory.
- ⚠Treating /proc/modules refcount as a guarantee of safety — the refcount can be zero momentarily during a race; always use try_module_get() rather than checking the count manually.
- ⚠Ignoring Module.symvers when building inter-module dependencies — without it, symbol version CRCs won't match and the dependent module will fail to load with 'disagrees about version of symbol'.
- ⚠Using static on a function you intend to EXPORT_SYMBOL — static functions have no external linkage and cannot be exported; the macro silently does nothing useful.
Practice
- 01easy
Write a 'hello world' kernel module that prints your name to the kernel log on load and 'goodbye' on unload. Build it, load it with insmod, verify the messages appear in dmesg, check /sys/module/ for its entry, then unload it with rmmod. Confirm the sysfs entry disappears.
- 02easy
Extend your hello module to accept two module parameters: an integer 'count' (default 1) and a string 'name' (default 'world'). The init function should print 'Hello, <name>!' exactly <count> times. Make 'count' writable via sysfs (perm 0644) and 'name' read-only (perm 0444). Test by loading with: sudo insmod hello.ko count=3 name=kernel, then read the parameters from /sys/module/hello/parameters/.
- 03medium
Build a two-module system: module_math exports an integer function multiply(int a, int b) using EXPORT_SYMBOL. module_calc depends on module_math and calls multiply() in its init function, printing the result. Load module_math first, then module_calc. Verify the dependency appears in 'lsmod'. Try rmmod module_math while module_calc is loaded — observe the EBUSY error. Unload in the correct order.
- 04medium
Write a module that creates a simple character device. The device should, when read, return a string 'kernel speaks\n'. Register the device with register_chrdev(), set .owner = THIS_MODULE in your file_operations, and implement .read. After loading, check /proc/devices for your major number, create a device node with mknod, read from it with 'cat /dev/mydev'. While cat is running (use a large count), try rmmod in another terminal — it should fail with EBUSY. Confirm refcnt in /sys/module/.
- 05hard
Generate a self-signed RSA-2048 signing key, sign your hello module with it, verify the signature with modinfo, and attempt to load it on a kernel with CONFIG_MODULE_SIG=y but CONFIG_MODULE_SIG_FORCE=n. Observe the taint flag difference between a signed and unsigned load by reading /proc/sys/kernel/tainted before and after. Document which bit (from Documentation/admin-guide/tainted-kernels.rst) is set.
Resources
The canonical, free, up-to-date guide specifically for writing kernel modules — covers everything in this module with working examples.
The classic reference. Chapters 1-3 cover module basics deeply; despite being from 2005, the LKM fundamentals are still accurate.
Official documentation for module signing — covers key generation, signing, and Secure Boot integration.
Practical walkthrough from the kernel community site — good for understanding the workflow around module development and first patches.
Deep dive into the sysfs/kobject infrastructure that underpins /sys/module/ — essential for understanding what you're looking at in sysfs.
Professional training slides (PDF) freely available — the kernel module labs are among the best structured hands-on exercises available.
Chapter 16 covers modules specifically; Love's writing is unusually clear for kernel topics and the book gives crucial context for why design decisions were made.
Official Kbuild documentation for out-of-tree modules — covers Makefiles, multi-file modules, and Module.symvers in detail.
Checkpoints — you can now…
- ✓You can write a minimal kernel module from scratch (init, exit, license macros) and successfully build it with a Kbuild Makefile against your running kernel's headers.
- ✓You can load and unload a module with insmod/rmmod and modprobe, explain the difference between them, and diagnose a vermagic mismatch or missing symbol error.
- ✓You can add module_param() parameters to a module, load it with custom values, and read/write parameters live through /sys/module/<name>/parameters/.
- ✓You can write a module that exports a symbol with EXPORT_SYMBOL, write a second module that uses it, build both with correct Module.symvers handling, and load/unload them in the correct dependency order.
- ✓You can explain why MODULE_LICENSE matters mechanically (GPL symbol access, taint), not just as a legal formality.
- ✓You can navigate /sys/module/ to inspect a loaded module's state, reference count, and parameters without any special tools.
- ✓You can describe the module signing process end-to-end: key generation, signing a .ko, the kernel's verification step, and what happens with an unsigned module under different CONFIG_MODULE_SIG settings.