Loading learning content...
As monolithic kernels grew in size and complexity, a significant innovation emerged: loadable kernel modules (LKMs). This evolution allows a monolithic kernel to dynamically load and unload code at runtime—adding drivers, file systems, and other subsystems without recompiling or rebooting.
The modular monolithic architecture attempts to capture some benefits of modularity while preserving the performance advantages of a monolithic design. When you insert a USB drive on Linux and it 'just works,' it's because the USB mass storage driver was dynamically loaded. When you mount an NFS share, the NFS module loads on demand.
In this concluding page of our monolithic kernel exploration, we examine how modularity works within the context of a monolithic kernel, its benefits and limitations, and how it addresses some of the challenges we identified earlier.
By the end of this page, you will:
• Understand how loadable kernel modules work in Linux • Grasp the mechanism of dynamic symbol resolution and linking • Analyze how modules provide flexibility without sacrificing performance • Examine module management and dependency handling • Evaluate the security implications of loadable modules
A loadable kernel module (LKM) is a piece of kernel code that can be loaded into a running kernel and unloaded when no longer needed. This concept originated in the early 1990s as kernels grew too large to fit in limited memory.
Core Concept
Modules are compiled separately from the main kernel but link against it at load time:
┌─────────────────────────────────────────────────────────┐
│ Running Kernel │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Permanent Kernel Core │ │
│ │ (scheduler, memory manager, core subsystems) │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ext4.ko │ │ nvidia.ko│ │ nfs.ko │ │ usb_hid │ │
│ │(loaded) │ │(loaded) │ │(unloaded)│ │(loaded) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ ↑ ↑ ↑ │
│ └───────────┴───────────┴───────────┘ │
│ Dynamically Linked at Runtime │
└─────────────────────────────────────────────────────────┘
Module Characteristics
Kernel modules have several key properties:
insmod or modprobermmod (if not in use)| Type | Examples | Use Case |
|---|---|---|
| Device drivers | nvidia.ko, e1000e.ko, xhci_hcd.ko | Hardware device support |
| File systems | ext4.ko, btrfs.ko, nfs.ko | Storage format support |
| Network protocols | ipv6.ko, nf_tables.ko, wireguard.ko | Networking features |
| Crypto algorithms | aes_x86_64.ko, sha256_ssse3.ko | Cryptographic acceleration |
| Security modules | apparmor.ko, selinux.ko | Security framework plugins |
| Block schedulers | bfq.ko, mq-deadline.ko | I/O scheduling policies |
Despite the 'modular' name, this is still monolithic architecture. Once loaded, modules run in the same address space as the rest of the kernel with full privileges. They are not isolated processes like in a microkernel. The modularity is about code organization and deployment—not security isolation.
Understanding how modules are structured and their lifecycle is essential for kernel development. Every module follows a specific structure dictated by the kernel's module framework.
Basic Module Structure
123456789101112131415161718192021222324252627282930313233343536
/* A minimal Linux kernel module */ #include <linux/module.h> /* Required for all modules */#include <linux/kernel.h> /* For printk, KERN_INFO, etc. */#include <linux/init.h> /* For __init, __exit macros */ /* Module metadata */MODULE_LICENSE("GPL");MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("A simple example kernel module");MODULE_VERSION("1.0"); /* Module parameters - can be set at load time */static int my_param = 42;module_param(my_param, int, 0644);MODULE_PARM_DESC(my_param, "An example parameter"); /* Initialization function - called when module is loaded */static int __init hello_init(void) { printk(KERN_INFO "Hello module: Loading with param=%d\n", my_param); /* Allocate resources, register with subsystems, etc. */ /* Return 0 on success, negative error code on failure */ return 0;} /* Cleanup function - called when module is unloaded */static void __exit hello_exit(void) { printk(KERN_INFO "Hello module: Unloading\n"); /* Free resources, unregister from subsystems, etc. */} /* Register init and exit functions with the kernel */module_init(hello_init);module_exit(hello_exit);Module File Format
Kernel modules are ELF (Executable and Linkable Format) files with special sections:
1234567891011121314151617181920212223
# Examining a kernel module with readelf$ readelf -S my_module.ko Section Headers: [Nr] Name Type Address Size [ 1] .text PROGBITS 0000000000000000 000001a0 [ 2] .rela.text RELA 0000000000000000 00000288 [ 3] .init.text PROGBITS 0000000000000000 00000040 [ 4] .exit.text PROGBITS 0000000000000000 00000020 [ 5] .rodata PROGBITS 0000000000000000 00000100 [ 6] .modinfo PROGBITS 0000000000000000 000000c0 [ 7] __param PROGBITS 0000000000000000 00000018 [ 8] .data PROGBITS 0000000000000000 00000008 [ 9] .bss NOBITS 0000000000000000 00000000 [10] .gnu.linkonce.this_module PROGBITS 0000000000000000 00000380 Key sections:.text → Module's compiled code.init.text → Initialization code (freed after init).exit.text → Cleanup code.modinfo → Module metadata (license, author, etc.)__param → Module parameter descriptors.gnu.linkonce.this_module → Module descriptor structureThe __init macro marks code that's only needed during initialization. This memory is freed after module_init() returns successfully. Similarly, __exit marks code only needed for cleanup—this can be omitted entirely in built-in (non-modular) kernel code. These optimizations reduce the kernel's memory footprint.
For modules to work, they need to call kernel functions. This is enabled through symbol exporting—the kernel exposes certain functions and variables that modules can use.
The Symbol Table
The kernel maintains a table of exported symbols that modules can reference:
12345678910111213141516171819202122232425262728293031323334
/* Exporting symbols from kernel to modules */ /* In kernel core code */void *kmalloc(size_t size, gfp_t flags) { /* implementation */}EXPORT_SYMBOL(kmalloc); /* Make available to all modules */ void important_function(void) { /* implementation */}EXPORT_SYMBOL_GPL(important_function); /* GPL modules only */ /* In a module using these symbols */#include <linux/slab.h> static int my_init(void) { void *ptr = kmalloc(1024, GFP_KERNEL); /* Uses exported symbol */ if (!ptr) return -ENOMEM; important_function(); /* Also uses exported symbol */ kfree(ptr); /* kfree is also exported */ return 0;} /* * Symbol resolution happens at module load time: * 1. Loader scans module's undefined symbols * 2. Looks up each in kernel symbol table * 3. Fills in actual addresses in module code * 4. If any symbol not found, load fails */Viewing Exported Symbols
The kernel's exported symbols can be examined:
# All exported symbols
$ cat /proc/kallsyms | head -20
ffffffff81000000 T startup_64
ffffffff81000000 T _stext
ffffffff81000040 T secondary_startup_64
...
# Module-specific symbols
$ modinfo -F depends nvidia
drm
# List symbols used by a module
$ nm my_module.ko | grep " U "
U kmalloc
U kfree
U printk
U __fentry__
| Export Macro | Visibility | Use Case |
|---|---|---|
| EXPORT_SYMBOL() | All modules | General-purpose kernel API |
| EXPORT_SYMBOL_GPL() | GPL modules only | GPL-specific APIs, encourages open source |
| EXPORT_SYMBOL_NS() | Within namespace | Subsystem-specific APIs |
| (not exported) | Kernel internal only | Implementation details, unstable APIs |
Module Versioning (modversions)
To prevent loading modules compiled against a different kernel version (which could cause crashes due to structure layout changes), Linux uses modversions:
$ modprobe nvidia
modprobe: ERROR: could not insert 'nvidia': Exec format error
# Often means symbol version mismatch - wrong kernel version
This protects against subtle ABI (Application Binary Interface) breakage but means modules must be recompiled for each kernel version.
Linux explicitly provides NO stable ABI for kernel modules. Any internal change can break out-of-tree modules. NVIDIA, ZFS, and other external modules must constantly adapt. This is intentional—it allows the kernel to evolve without being constrained by external dependencies. The price is paid by out-of-tree module maintainers.
Modules often depend on other modules. The modprobe tool handles automatic dependency resolution and loading.
Dependency Tracking
Modules export symbols that other modules may use, creating a dependency graph:
12345678910111213141516171819202122
# View module dependencies$ lsmod | head -10Module Size Used bynvidia_uvm 1368064 0nvidia_drm 73728 3nvidia_modeset 1236992 5 nvidia_drmnvidia 56471552 165 nvidia_uvm,nvidia_modesetdrm_kms_helper 184320 1 nvidia_drmdrm 557056 7 drm_kms_helper,nvidia_drmvideo 49152 1 nvidia_modeset # Dependency tree for a module$ modprobe --show-depends btrfsinsmod /lib/modules/$(uname -r)/kernel/lib/libcrc32c.koinsmod /lib/modules/$(uname -r)/kernel/lib/raid6/raid6_pq.koinsmod /lib/modules/$(uname -r)/kernel/lib/zlib_deflate/zlib_deflate.koinsmod /lib/modules/$(uname -r)/kernel/lib/zstd/zstd_compress.koinsmod /lib/modules/$(uname -r)/kernel/fs/btrfs/btrfs.ko # Dependencies are stored in modules.dep$ head /lib/modules/$(uname -r)/modules.depkernel/fs/btrfs/btrfs.ko: kernel/lib/libcrc32c.ko kernel/lib/raid6/raid6_pq.ko ...Automatic Module Loading
Linux can automatically load modules when needed:
udev — When hardware is detected, udev matches device IDs to module aliases and loads appropriate drivers
Kernel request_module() — Kernel code can request a module by name:
request_module("nfs"); // Kernel loads the NFS module
$ mount -t ext4 /dev/sda1 /mnt # Loads ext4.ko if not present
$ ip link add wg0 type wireguard # Loads wireguard.ko
Module Aliases
Modules declare aliases that map hardware IDs to module names:
123456789101112131415161718192021222324252627
/* Module aliases for automatic hardware detection */ /* In a network driver */static const struct pci_device_id my_pci_ids[] = { { PCI_DEVICE(0x8086, 0x1533) }, /* Intel I210 */ { PCI_DEVICE(0x8086, 0x1539) }, /* Intel I211 */ { 0, }};MODULE_DEVICE_TABLE(pci, my_pci_ids); /* This generates aliases like: * alias pci:v00008086d00001533sv*sd*bc*sc*i* my_driver * * When a PCI device with vendor=0x8086, device=0x1533 is found, * udev matches the alias and loads my_driver.ko */ /* USB example */static const struct usb_device_id my_usb_ids[] = { { USB_DEVICE(0x046d, 0xc52b) }, /* Logitech Unifying Receiver */ { 0, }};MODULE_DEVICE_TABLE(usb, my_usb_ids); /* $ cat /lib/modules/$(uname -r)/modules.alias | grep 8086:1533 * alias pci:v00008086d00001533sv*sd*bc*sc*i* igb */When you plug in a USB device, the kernel detects it, udev matches the vendor/product ID against module aliases, loads the appropriate driver module, and the device works—all within seconds, without user intervention. This is the modular system at its best.
The modular monolithic design provides significant benefits that address some of the challenges of pure monolithic kernels.
Memory Efficiency
| Configuration | Kernel Size | Runtime Memory | Boot Time |
|---|---|---|---|
| All drivers built-in | 50-100 MB | High (all loaded) | Slow (large image) |
| All drivers as modules | 5-10 MB | Low (load on demand) | Fast (small vmlinuz) |
| Distro typical (mixed) | 10-15 MB | Moderate | Moderate |
| Embedded (custom) | 2-5 MB | Minimal | Very fast |
Development and Testing Benefits
rmmod mymod && insmod mymod.ko12345678910111213141516171819202122232425262728
#!/bin/bash# Developer workflow for kernel module development # Initial development cyclemake -C /lib/modules/$(uname -r)/build M=$PWD modules# Compile time: ~5 seconds for one module # Load for testingsudo rmmod my_driver 2>/dev/nullsudo insmod my_driver.ko debug=1# Load time: <1 second # Test the driverdmesg | tail -20./test_driver # Found a bug? Fix, recompile, reloadvim my_driver.cmake -C /lib/modules/$(uname -r)/build M=$PWD modulessudo rmmod my_driver && sudo insmod my_driver.ko # Compare to non-modular workflow:# 1. Edit driver in kernel tree# 2. Recompile entire kernel (10-30 minutes)# 3. Install new kernel# 4. Reboot (2-5 minutes)# 5. Test# Total: 15-40 minutes per iteration vs <1 minuteDeployment and Maintenance Benefits
DKMS automatically rebuilds out-of-tree modules when the kernel is upgraded. When you install a new kernel, DKMS triggers recompilation of modules like NVIDIA drivers, ensuring they continue working without manual intervention.
The ability to dynamically load kernel code is a powerful feature—and a significant security consideration. Kernel modules have the same privileges as the rest of the kernel, making module loading a high-value target for attackers.
The Threat Model
1234567891011121314151617181920212223242526272829303132333435
/* WARNING: Educational example only - showing attack concepts */ /* Rootkits often hook system calls to hide themselves */ /* Original: returns list of running processes */asmlinkage long (*orig_getdents64)(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count); /* Hooked: filters out processes we want to hide */asmlinkage long hooked_getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count) { long ret = orig_getdents64(fd, dirp, count); /* Filter out entries containing "malicious_process" */ /* User-space tools like ps, top won't see the hidden processes */ return ret;} /* Hiding from lsmod by unlinking from module list */static void hide_module(void) { struct module *mod = THIS_MODULE; /* Remove from module list - lsmod won't show it */ list_del(&mod->list); /* Remove from sysfs - /sys/module/name won't exist */ kobject_del(&mod->mkobj.kobj);} /* * This is why module loading must be tightly controlled! * Anyone who can load modules has complete system control. */Security Mitigations
Linux provides several mechanisms to control module loading:
| Mechanism | Effect | Configuration |
|---|---|---|
| MODULES=n | Build kernel without module support | CONFIG_MODULES=n |
| MODULE_SIG | Require cryptographic signature on modules | CONFIG_MODULE_SIG_FORCE=y |
| Lockdown | Prevent unsigned module loading | kernel lockdown=integrity |
| SecureBoot | UEFI enforces signed bootchain + kernel + modules | Hardware + distro support |
| Disable loading | Disable module loading at runtime | echo 1 > /proc/sys/kernel/modules_disabled |
| CAP_SYS_MODULE | Require capability for module operations | Capability-based access control |
Module Signing
Modern secure systems require modules to be cryptographically signed:
# Sign a module during build
$ scripts/sign-file sha256 certs/signing_key.pem certs/signing_key.x509 my_module.ko
# Kernel verifies signature at load time
$ modprobe my_module
# If signature invalid or missing, and CONFIG_MODULE_SIG_FORCE=y:
# modprobe: ERROR: could not insert 'my_module': Required key not available
Secure Boot Chain
This chain breaks if any link is compromised—but it significantly raises the bar for attackers.
If an attacker gains root access, they can potentially load malicious modules (unless properly locked down). Module security is one reason why root compromise is considered total system compromise. Defense in depth—including module signing, lockdown mode, and capability restrictions—helps, but cannot fully prevent a determined attacker with root access.
The modular monolithic approach can be compared to alternative kernel architectures and their approaches to extensibility.
Modular Monolithic vs. Microkernel
Comparison with eBPF
eBPF (extended Berkeley Packet Filter) is a newer approach that allows running sandboxed code in the kernel:
| Aspect | Traditional LKM | eBPF |
|---|---|---|
| Language | C (full kernel API) | Restricted C, verified bytecode |
| Privileges | Full kernel access | Sandboxed, limited helpers |
| Verification | None (trust developer) | Static verification before load |
| Safety | Can crash kernel | Cannot crash kernel by design |
| Performance | Native code | JIT compiled, slight overhead |
| Capabilities | Anything kernel can do | Network, tracing, security, limited |
| Update | Unload/reload | Atomic replacement |
12345678910111213141516171819202122232425262728293031323334353637383940
/* Traditional LKM: Full kernel access, no restrictions */static int my_module_init(void) { struct task_struct *task; /* Can iterate through ALL processes */ for_each_process(task) { /* Can access ANY data */ printk("%s [%d]\n", task->comm, task->pid); /* Can MODIFY anything */ task->prio = 100; /* Dangerous! */ } /* Can crash the kernel easily */ int *ptr = NULL; *ptr = 42; /* Kernel panic! */ return 0;} /* eBPF: Sandboxed, verified, restricted */SEC("tracepoint/sched/sched_process_exec")int trace_exec(struct trace_event_raw_sched_process_exec *ctx) { /* Can only use approved helpers */ bpf_printk("Process exec: %s", ctx->filename); /* Cannot access arbitrary memory */ /* Cannot modify kernel data structures */ /* Cannot crash the kernel */ /* Verifier ensures safety before loading */ return 0;} /* * eBPF represents a middle ground: * - More flexible than microkernel servers * - Safer than traditional modules * - Less capable than full kernel modules */The Evolution Continues
The kernel is evolving with new extensibility mechanisms:
These represent attempts to get closer to microkernel benefits while staying within the monolithic framework.
Rust in the Linux kernel is a significant development—it brings memory safety guarantees to kernel code. While still monolithic (Rust modules run in kernel space), Rust eliminates classes of bugs (use-after-free, buffer overflow) at compile time. This may be the most impactful change to kernel development in decades.
We've completed our deep dive into monolithic kernels by examining how modularity enhances the traditional monolithic design without sacrificing its core benefits.
Module Complete: Monolithic Kernels
We've now thoroughly examined monolithic kernel architecture:
With this foundation, you're prepared to explore alternative architectures—microkernels that take the opposite approach, prioritizing isolation over performance, and hybrid kernels that attempt to balance both concerns.
You now possess a comprehensive understanding of monolithic kernel architecture—from fundamental concepts through to practical implementation details. This knowledge forms the foundation for understanding operating system design, kernel development, and system administration. In the next module, we'll explore the microkernel architecture—the philosophical opposite of monolithic design.