Loading content...
The debate between monolithic and microkernel architectures often presents a false dichotomy. Must we choose between the raw performance of having everything in one kernel address space, and the flexibility of modular, isolated components?
Loadable Kernel Modules (LKMs) offer a pragmatic middle ground. They allow a running kernel to be extended dynamically—adding device drivers, file systems, network protocols, and system features—without rebooting or recompiling the kernel. The code runs in kernel space with full privileges (like a monolithic kernel), but it can be loaded and unloaded on demand (like modular components).
This approach has become the dominant paradigm for modern general-purpose operating systems. Linux, Windows, macOS, and FreeBSD all rely heavily on loadable modules. Understanding how they work, and what security tradeoffs they entail, is essential for anyone working with operating systems.
By the end of this page, you will understand how loadable kernel modules work internally, explore the Linux kernel module system in depth, learn module management practices, examine the security implications of dynamic kernel extension, and appreciate how modules enable the modern Linux ecosystem.
A loadable kernel module (LKM) is a piece of kernel code that can be loaded into or unloaded from a running kernel at runtime. Unlike static kernel code compiled directly into the kernel binary, modules exist as separate object files (.ko on Linux) that the kernel dynamically links when needed.
What Makes Modules Different from User-Space Programs?
| Aspect | User-Space Program | Kernel Module |
|---|---|---|
| Execution Context | Runs in user mode with restricted privileges | Runs in kernel mode with full hardware access |
| Address Space | Has its own isolated address space | Shares the kernel's address space |
| Memory Protection | Protected from other processes and kernel | No protection—can access any kernel memory |
| System Calls | Must ask kernel for resources via syscalls | Directly manipulates kernel structures |
| Crash Behavior | Only affects the crashing program | Can crash the entire system |
| Loading Mechanism | exec() family creates new process | insmod/modprobe links into running kernel |
The Module Lifecycle:
A kernel module goes through distinct phases:
Compilation: Module source is compiled into an object file (.ko) compatible with the running kernel version.
Loading: The kernel's module loader reads the .ko file, resolves symbols (functions/variables the module uses from the kernel), allocates memory, and links the module into the kernel's address space.
Initialization: The module's init function is called, allowing it to register with kernel subsystems (e.g., register a device driver, mount a filesystem type).
Operation: The module's functions are called by the kernel as needed (e.g., when its device is accessed, when its filesystem is used).
Cleanup: Before unloading, the module's exit function is called to deregister from subsystems and release resources.
Unloading: The module is unlinked from the kernel's address space and its memory is freed.
Linux kernel configuration lets you choose for each feature: Y (built-in to kernel), M (loadable module), or N (excluded). Built-in code is always present; modules are loaded as needed. Critical drivers (like the root filesystem driver) must be built-in; optional drivers are typically modules. This flexibility lets one kernel binary support diverse hardware configurations.
Linux's module system is the most widely studied and used in the world. Let's examine its architecture and implementation in detail.
Module File Format:
Linux kernel modules (.ko files) are ELF (Executable and Linkable Format) object files containing:
.text): The compiled functions of the module.data, .bss): Global and static variables12345678910111213141516171819202122232425
#include <linux/init.h>#include <linux/module.h>#include <linux/kernel.h> MODULE_LICENSE("GPL");MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("A simple hello world LKM");MODULE_VERSION("1.0"); // Initialization function - called when module is loadedstatic int __init hello_init(void){ printk(KERN_INFO "Hello, Kernel World!\n"); return 0; // 0 indicates success} // Cleanup function - called when module is unloadedstatic void __exit hello_exit(void){ printk(KERN_INFO "Goodbye, Kernel World!\n");} // Register the init and exit functionsmodule_init(hello_init);module_exit(hello_exit);Key Components Explained:
module_init() and module_exit(): Macros that mark the initialization and cleanup functions. The kernel calls hello_init() when the module loads and hello_exit() when it unloads.
__init and __exit: Section markers. __init tells the kernel the function is only needed once (at load time), so its memory can be freed afterward. __exit marks functions not needed if the module is built-in (can't unload built-in code).
printk(): The kernel's printf equivalent. KERN_INFO is a log level. Output goes to the kernel log (viewable via dmesg).
MODULE_LICENSE(): Critical! The kernel restricts some features for non-GPL modules. 'Tainted kernel' warnings appear if proprietary modules are loaded.
Building the Module:
123456789
obj-m += hello.o KERNEL_DIR := /lib/modules/$(shell uname -r)/build all: make -C $(KERNEL_DIR) M=$(PWD) modules clean: make -C $(KERNEL_DIR) M=$(PWD) cleanManaging Modules:
| Command | Purpose | Example |
|---|---|---|
insmod | Insert a module (no dependency resolution) | insmod hello.ko |
rmmod | Remove a module | rmmod hello |
modprobe | Insert module with dependency resolution | modprobe bluetooth |
modprobe -r | Remove module and unused dependencies | modprobe -r bluetooth |
lsmod | List loaded modules | lsmod | grep hello |
modinfo | Display module information | modinfo hello.ko |
modprobe vs. insmod: insmod is low-level—it loads exactly one module by path. modprobe is high-level—it reads dependency information from /lib/modules/$(uname -r)/modules.dep and loads all required modules in the correct order.
After loading a module, use dmesg | tail to see its printk output. The kernel ring buffer stores these messages. For real-time monitoring during module development: dmesg -w (follow mode) or journalctl -kf on systemd systems.
Kernel modules don't exist in isolation—they interact with the kernel and each other through exported symbols. Understanding symbol management is crucial for kernel development.
What Are Kernel Symbols?
A symbol is a named address—typically a function or global variable. The kernel maintains a symbol table listing all functions and variables available for use by modules.
Categories of Symbols:
| Category | Description | Visibility |
|---|---|---|
| Internal | Functions/variables used only within a source file | Static, not exported |
| Exported | Available to all modules via EXPORT_SYMBOL() | Public kernel API |
| GPL-Only Exported | Available only to GPL-licensed modules via EXPORT_SYMBOL_GPL() | GPL modules only |
| Module-Local | Used within a multi-file module but not exported externally | Module-private |
123456789101112131415161718192021222324
#include <linux/module.h>#include <linux/kernel.h> // A function that will be available to all modulesint public_function(int x){ printk(KERN_INFO "public_function called with %d\n", x); return x * 2;}EXPORT_SYMBOL(public_function); // A function only available to GPL-licensed modulesint gpl_only_function(int x){ printk(KERN_INFO "gpl_only_function called with %d\n", x); return x * 3;}EXPORT_SYMBOL_GPL(gpl_only_function); // This function is NOT exported - usable only within this modulestatic int internal_function(int x){ return x + 1;}Module Dependencies:
When a module uses symbols from another module, it creates a dependency. The Linux kernel tracks these dependencies:
modprobe reads /lib/modules/$(uname -r)/modules.dep to determine load orderExample Dependency Chain:
bluetooth depends on rfkill, crypto
├── rfkill depends on cfg80211
│ └── cfg80211 (no dependencies)
└── crypto (core module, no dependencies)
When you run modprobe bluetooth, the system loads cfg80211, rfkill, and crypto first (in correct order), then bluetooth.
Viewing Dependencies:
12345678910111213141516
# View dependencies declared by a module$ modinfo -F depends bluetoothrfkill,ecdh_generic # View the current usage count (who's using this module)$ lsmod | grep bluetoothbluetooth 684032 44 bnep,btrtl,btintel,btbcm,btusb # The number (44) shows how many other modules/users depend on it # View the full dependency tree$ modprobe --show-depends bluetoothinsmod /lib/modules/5.15.0/kernel/lib/crypto/libaes.koinsmod /lib/modules/5.15.0/kernel/crypto/aes_generic.koinsmod /lib/modules/5.15.0/kernel/net/rfkill/rfkill.koinsmod /lib/modules/5.15.0/kernel/net/bluetooth/bluetooth.koModules cannot be unloaded while in use. If a filesystem module is mounted anywhere, it cannot be removed. If a device driver has open file handles, it stays loaded. The 'Used by' column in lsmod shows the reference count. Trying to rmmod a module with non-zero count fails with 'Module is in use'.
Kernel modules serve diverse purposes beyond simple device drivers. Let's categorize the major types:
| Category | Purpose | Examples |
|---|---|---|
| Device Drivers | Interface between hardware and kernel | e1000e (Intel NIC), i915 (Intel graphics), snd-hda-intel (audio) |
| File Systems | Implement filesystem types | ext4, btrfs, ntfs3, fuse (user-space FS support) |
| Network Protocols | Implement network functionality | nf_conntrack (connection tracking), ip_tables (netfilter), wireguard |
| System Facilities | Provide kernel features | kvm (virtualization), cgroups, audit (security auditing) |
| Crypto Algorithms | Cryptographic implementations | aes_x86_64, sha256_generic, ecb, xts |
| Security Modules | Implement security frameworks | apparmor, selinux, tomoyo, smack |
| Storage Targets | Block device management | dm-crypt (encryption), dm-raid (RAID), nbd (network block) |
Device Drivers in Detail:
Device drivers are the most common type of module. They fall into three categories based on the type of device interface:
1. Character Device Drivers (char)
/dev/random, /dev/null2. Block Device Drivers (block)
3. Network Device Drivers (net)
Filesystem modules register with the VFS (Virtual File System) layer, providing implementations of operations like read, write, lookup, and mkdir. The VFS provides a uniform interface to applications while filesystems handle storage-specific details. This is why you can 'cat' a file regardless of whether it's on ext4, NTFS, or even a network filesystem.
Kernel modules can accept parameters at load time—similar to command-line arguments for user programs. This allows configuration without recompiling.
Declaring Parameters:
123456789101112131415161718192021222324252627282930313233343536
#include <linux/init.h>#include <linux/module.h>#include <linux/moduleparam.h> MODULE_LICENSE("GPL"); // Integer parameter with default valuestatic int buffer_size = 4096;module_param(buffer_size, int, 0644);MODULE_PARM_DESC(buffer_size, "Size of internal buffer (default: 4096)"); // String parameterstatic char *device_name = "mydevice";module_param(device_name, charp, 0644);MODULE_PARM_DESC(device_name, "Name of the device to create"); // Boolean parameterstatic bool enable_debug = false;module_param(enable_debug, bool, 0644);MODULE_PARM_DESC(enable_debug, "Enable debug logging (default: false)"); // Array parameterstatic int ports[4] = { 8080, 8081, 8082, 8083 };static int num_ports = 4;module_param_array(ports, int, &num_ports, 0644);MODULE_PARM_DESC(ports, "Array of port numbers to listen on"); static int __init params_init(void){ printk(KERN_INFO "Buffer size: %d\n", buffer_size); printk(KERN_INFO "Device name: %s\n", device_name); printk(KERN_INFO "Debug mode: %s\n", enable_debug ? "ON" : "OFF"); return 0;} module_init(params_init);Parameter Permissions (mode):
The third argument to module_param() is a file permission mode (like Unix file permissions):
| Mode | Meaning | File in sysfs |
|---|---|---|
0 | Not visible in sysfs | No file created |
0444 | Read-only for everyone | /sys/module/name/parameters/param |
0644 | Read for all, write for root | Allows runtime modification |
0600 | Read/write for root only | Secure parameters |
Loading with Parameters:
123456789101112131415
# Load module with custom parameters$ sudo insmod params.ko buffer_size=8192 device_name="custom" enable_debug=1 # Using modprobe (parameters go after module name)$ sudo modprobe mymodule buffer_size=8192 # View current parameter values via sysfs$ cat /sys/module/params/parameters/buffer_size8192 # Modify parameter at runtime (if permissions allow)$ echo 16384 | sudo tee /sys/module/params/parameters/buffer_size # Persist parameters across reboots via /etc/modprobe.d/$ echo "options mymodule buffer_size=8192" | sudo tee /etc/modprobe.d/mymodule.confUse modinfo -p modulename to see all parameters a module accepts along with their descriptions. The sysfs filesystem (/sys/module/modulename/parameters/) shows current values of parameters with non-zero permissions.
Loadable kernel modules present significant security concerns. A loaded module has complete access to the kernel—it can read/write any memory, intercept any system call, and hide any activity. This makes modules a prime target and tool for attackers.
knark and adore were kernel modules./lib/modules/ provides persistent compromise.Defense Mechanisms:
Linux provides several security features to control module loading:
1. Module Signing Since Linux 3.7, modules can be cryptographically signed, and the kernel can be configured to only load modules signed with trusted keys.
# Check if kernel requires signed modules
$ grep CONFIG_MODULE_SIG /boot/config-$(uname -r)
CONFIG_MODULE_SIG=y
CONFIG_MODULE_SIG_FORCE=y # If set, unsigned modules are rejected
2. Lockdown Mode Linux 5.4+ includes a lockdown feature that can restrict module loading, direct hardware access, and other potentially dangerous operations. Often used with Secure Boot.
3. Disabling Module Loading Once initial boot completes, the system can disable all further module loading:
# Disable module loading permanently (until reboot)
$ echo 1 > /proc/sys/kernel/modules_disabled
4. Restrict Capabilities
The CAP_SYS_MODULE capability controls who can load modules. Removing this from all users prevents module loading even by root in some configurations.
5. SELinux/AppArmor Policies Mandatory Access Control can restrict which programs can load modules, even if they have root privileges.
With UEFI Secure Boot, the kernel must be signed, and in turn only accepts signed modules. If you compile custom modules, you must sign them with a key enrolled in the system's MOK (Machine Owner Key) database—or disable Secure Boot. This creates friction but significantly improves security against kernel-level malware.
The Tainted Kernel:
Linux tracks 'taint' flags indicating potentially problematic conditions:
| Flag | Meaning |
|---|---|
P | Proprietary module loaded |
G | All modules are GPL |
F | Module was force-loaded |
O | Out-of-tree module loaded |
E | Unsigned module in signature-enforcing kernel |
C | Staging driver loaded |
Check taint status: cat /proc/sys/kernel/tainted
A tainted kernel is not 'broken,' but kernel developers may refuse to debug bug reports from tainted systems, as proprietary/unsigned modules could be the cause.
Effective module management is essential for system administration. Here are key practices and techniques:
Auto-Loading Modules:
Linux auto-loads modules based on hardware detection and explicit requests:
udev rules trigger when hardware is detected, calling modprobe with the device's modalias/etc/modprobe.d/ can map names to modulesBlacklisting Modules:
Sometimes you need to prevent a module from loading (buggy driver, security concern, conflicting drivers):
1234567891011
# Prevent a module from auto-loadingblacklist nouveau # Blacklist open-source nvidia driver # Use 'install' to prevent loading entirely (even manual loading)install nouveau /bin/false # Softdep can modify dependency handlingsoftdep snd_hda_intel pre: snd_hda_codec_hdmi # Configure module options (applied when module loads)options iwlwifi 11n_disable=1DKMS (Dynamic Kernel Module Support):
When you install kernel updates, modules compiled for the old kernel don't work. DKMS automatically recompiles out-of-tree modules for each new kernel:
# Register a module with DKMS
$ sudo dkms add -m mymodule -v 1.0
# Build for current kernel
$ sudo dkms build -m mymodule -v 1.0
# Install for current kernel
$ sudo dkms install -m mymodule -v 1.0
# On kernel upgrade, DKMS automatically rebuilds
NVIDIA and VirtualBox drivers commonly use DKMS to survive kernel updates.
Debugging Module Issues:
12345678910111213141516171819
# Check kernel log for module errors$ dmesg | grep -i error # Verbose module loading (shows symbol resolution)$ sudo modprobe -v bluetooth # Check why a module load failed$ sudo modprobe -f mymodule # Force load (dangerous)$ dmesg | tail -20 # Check for error messages # View module section sizes and memory usage$ size /lib/modules/$(uname -r)/kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko # Check module binary info$ file /lib/modules/$(uname -r)/kernel/drivers/net/wireless/intel/iwlwifi/iwlwifi.koELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[...], not stripped # Examine module symbols$ nm /path/to/module.ko | grep -i initWhen a module fails to load: 1) Check dmesg for errors. 2) Verify module is built for current kernel version (modinfo shows vermagic). 3) Check dependencies are satisfied (modprobe --show-depends). 4) Verify module isn't blacklisted. 5) Check if Secure Boot requires signing. 6) Ensure required firmware is present in /lib/firmware/.
We've explored the loadable kernel module system in depth—the mechanism that gives monolithic kernels microkernel-like flexibility. Let's consolidate the key insights:
What's Next: Hybrid Operating Systems
The final page of this module explores hybrid architectures—systems that combine monolithic, microkernel, and modular approaches to achieve optimal tradeoffs for their specific domains. We'll examine Windows NT's architecture (which influenced all modern Windows versions), macOS's XNU kernel (combining Mach and BSD), and Android's Linux-based hybrid design.
You'll see how real-world systems pragmatically blend architectural ideas rather than adhering to pure types.
You now understand how loadable kernel modules work—from ELF format and symbol resolution through init/exit functions and parameter handling. You can write, build, load, and debug kernel modules, and you understand the security implications of dynamic kernel extension. This knowledge is essential for driver development, system customization, and security analysis.