Loading content...
A device driver is just a file on disk—a .ko (kernel object) file containing compiled code and data. But how does this static file transform into active kernel code managing real hardware? The process of driver loading involves sophisticated mechanisms that link the driver into the running kernel's address space, resolve symbol references, execute initialization code, and integrate the driver with kernel subsystems.
Understanding driver loading is essential for system administration, debugging driver issues, and developing drivers that work correctly in all loading scenarios. This page takes you through the complete journey from dormant .ko file to active kernel module.
By the end of this page, you will understand the module loading process from insmod to running code, how symbols are resolved between modules and the kernel, the initialization and cleanup sequences, automatic driver loading through udev and modalias, module parameters and dependencies, and how to troubleshoot loading failures. This knowledge is essential for system administrators and driver developers alike.
The Linux kernel supports two methods of including driver code: built-in (compiled directly into the kernel image) and loadable modules (separate files loaded at runtime). Loadable modules provide significant advantages for flexibility and resource efficiency.
Benefits of Loadable Modules:
| Aspect | Loadable Module (M) | Built-In (Y) |
|---|---|---|
| Kernel Size | Smaller base kernel | Larger kernel image |
| Boot Time | Slower (load on demand) | Faster (already present) |
| Memory | Used only when loaded | Always resident |
| Hot-Reload | Possible (rmmod + insmod) | Requires reboot |
| Dependencies | Must be available on disk | All linked into kernel |
| Use Case | Optional hardware, development | Core functionality, init-time requirements |
1234567891011121314151617181920212223242526272829303132333435363738394041
#include <linux/module.h>#include <linux/init.h> /* Module initialization - called when module is loaded */static int __init mydriver_init(void){ pr_info("MyDriver: Initializing\n"); /* Perform initialization: * - Register with bus subsystem * - Allocate global resources * - Create device files */ return 0; /* Success; non-zero = failure */} /* Module cleanup - called when module is unloaded */static void __exit mydriver_exit(void){ pr_info("MyDriver: Exiting\n"); /* Perform cleanup: * - Unregister from bus subsystem * - Free global resources * - Remove device files */} /* Register init and exit functions */module_init(mydriver_init);module_exit(mydriver_exit); /* Module metadata - stored in .modinfo section */MODULE_LICENSE("GPL"); /* Required for GPL symbols */MODULE_AUTHOR("Your Name <email@example.com>");MODULE_DESCRIPTION("Example Device Driver");MODULE_VERSION("1.0"); /* Alias for automatic loading */MODULE_ALIAS("pci:v00001234d00005678sv*sd*bc*sc*i*");The __init and __exit Markers:
The __init marker tells the kernel that the function is only needed during initialization. After module loading completes, the kernel can free the memory occupied by __init functions and data. Similarly, __exit functions are discarded for built-in drivers (which can never be unloaded).
static int __init my_init(void) /* Freed after init */
static char __initdata buffer[1024]; /* Data freed after init */
static void __exit my_exit(void) /* Discarded if built-in */
For modules, __exit functions are preserved for rmmod. For built-in code, they're completely omitted from the binary.
MODULE_LICENSE("GPL") is not just documentation—it affects what symbols your module can use. Many kernel functions are exported only to GPL modules (EXPORT_SYMBOL_GPL). Using a non-GPL license with these symbols causes loading failures. Also, mixing GPL and proprietary code has legal implications.
When you run insmod mydriver.ko or modprobe mydriver, a complex sequence of events transforms the dormant file into active kernel code. Understanding this process helps diagnose loading failures and write drivers that load cleanly.
Loading Stages in Detail:
1. File Reading (User Space):
insmod reads the entire .ko file into memory. The file is an ELF (Executable and Linkable Format) object containing code, data, symbols, and relocation information.
2. System Call (User → Kernel):
The init_module() or finit_module() system call passes the module data to the kernel. This requires CAP_SYS_MODULE capability, typically restricted to root.
3. ELF Verification: The kernel validates the ELF structure, checks architecture compatibility, and verifies the module isn't already loaded.
4. Memory Allocation: Kernel memory is allocated for the module's text (code), data (initialized variables), and BSS (uninitialized variables) sections. This memory comes from the vmalloc region.
5. Symbol Resolution: The module references external symbols (kernel functions, exported symbols from other modules). The kernel resolves these references to actual addresses.
6. Relocation: The code is adjusted for its actual load address. All absolute addresses in the code are patched based on where the module was loaded.
7. Version Checking: If CONFIG_MODVERSIONS is enabled, CRC checksums of symbol signatures are compared to ensure ABI compatibility.
8. Initialization:
The kernel calls the module's init function (registered via module_init()). This is where the driver sets up devices, registers with subsystems, etc.
9. Registration:
The module is added to the list of loaded modules, entries are created in /sys/module/, and the module is ready for use.
1234567891011121314151617181920212223242526272829303132333435363738394041
# Basic module loading (doesn't handle dependencies)sudo insmod ./mydriver.ko # Load with parameterssudo insmod ./mydriver.ko debug=1 buffer_size=8192 # View load messagesdmesg | tail -10 # Smart loading - resolves dependencies automatically# Searches /lib/modules/$(uname -r)/sudo modprobe mydriver # Unload modulesudo rmmod mydriver # Or with modprobe (handles reverse dependencies)sudo modprobe -r mydriver # View loaded moduleslsmod # Detailed module infomodinfo ./mydriver.komodinfo mydriver # If installed in /lib/modules/ # Example modinfo output:# filename: /lib/modules/5.10.0/kernel/drivers/mydriver.ko# license: GPL# author: Your Name <email@example.com># description: Example Device Driver# version: 1.0# depends: crc32# vermagic: 5.10.0 SMP mod_unload modversions# parm: debug:Enable debug logging (int) # Force load (skip version check - DANGEROUS)sudo insmod -f ./mydriver.ko # List module dependenciesmodprobe --show-depends mydriverinsmod loads exactly the file you specify and fails if dependencies are missing. modprobe reads /lib/modules/$(uname -r)/modules.dep to find and load dependencies first. For development, insmod with explicit .ko path is common. For production, use modprobe with properly installed modules.
When a module references an external function or variable, the kernel must find the actual address to patch into the module's code. This symbol resolution process is central to how modules integrate with the kernel.
Symbol Categories:
The kernel maintains a symbol table containing all kernel functions and exports from loaded modules. During loading, the module loader searches this table to resolve each undefined symbol.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
/* Module A: Exports symbols for other modules */ #include <linux/module.h>#include <linux/export.h> /* Public function - available to any module */void my_library_function(int param){ pr_info("my_library_function called with %d\n", param);}EXPORT_SYMBOL(my_library_function); /* GPL-only function - only GPL modules can use this */int my_gpl_function(void){ /* Uses GPL-only kernel internals */ return 0;}EXPORT_SYMBOL_GPL(my_gpl_function); /* Documentation symbol table (visible in /proc/kallsyms) */ /* * Symbol visibility: * EXPORT_SYMBOL - Available to all modules * EXPORT_SYMBOL_GPL - Only GPL-licensed modules * EXPORT_SYMBOL_NS - Available within namespace * EXPORT_SYMBOL_NS_GPL - GPL + namespace restriction * (no export) - Module-internal only */ MODULE_LICENSE("GPL"); /* ================================================ */ /* Module B: Uses symbols from Module A and kernel */ #include <linux/module.h> /* Declaration for external symbol */extern void my_library_function(int param); static int __init user_module_init(void){ /* Call kernel exported function */ pr_info("Calling kernel function\n"); /* printk is exported */ /* Call Module A's exported function */ my_library_function(42); return 0;} module_init(user_module_init);MODULE_LICENSE("GPL");| Error Message | Cause | Solution |
|---|---|---|
| Unknown symbol 'xyz' | Symbol not exported or module not loaded | Load dependency first, or add EXPORT_SYMBOL |
| disagrees about version of symbol | Kernel version mismatch | Rebuild module against correct kernel headers |
| module verification failed | Signature check failed | Sign module or disable signing enforcement |
| module_layout mismatch | CONFIG options differ from kernel | Rebuild with matching kernel config |
| GPL-only symbol 'xyz' | Using GPL symbol with wrong license | Change MODULE_LICENSE to "GPL" |
Viewing Symbols:
# View all kernel symbols (requires root or sysctl permission)
sudo cat /proc/kallsyms | grep my_function
# View symbols exported by a module
nm mydriver.ko
# View symbols a module needs (undefined references)
nm -u mydriver.ko
# View module's symbol table section
objdump -t mydriver.ko
# View the export table in the kernel
cat /proc/kallsyms | grep " E " | head
Symbol Namespaces:
Modern kernels support symbol namespaces to limit symbol visibility:
/* Export into a namespace */
EXPORT_SYMBOL_NS(my_function, MY_DRIVER_API);
/* Import from namespace (module must declare this) */
MODULE_IMPORT_NS(MY_DRIVER_API);
This prevents unintended dependencies on internal symbols and clarifies API boundaries.
CONFIG_MODVERSIONS adds CRC checksums of function signatures to prevent loading modules compiled for incompatible kernels. If a function's parameters change, the CRC changes, and loading fails with 'disagrees about version.' This catches many ABI incompatibilities at load time rather than causing crashes at runtime.
Modules often need configuration that varies by system or use case. Module parameters allow users to customize driver behavior at load time without recompiling. Parameters appear in sysfs and can sometimes be modified at runtime.
Parameter Use Cases:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
#include <linux/module.h>#include <linux/moduleparam.h> /* Simple integer parameter */static int debug = 0;module_param(debug, int, 0644); /* sysfs permissions */MODULE_PARM_DESC(debug, "Enable debug logging (0=off, 1=on)"); /* Unsigned integer with default */static unsigned int buffer_size = 4096;module_param(buffer_size, uint, 0444); /* Read-only in sysfs */MODULE_PARM_DESC(buffer_size, "DMA buffer size in bytes"); /* String parameter */static char *device_name = "mydev";module_param(device_name, charp, 0644);MODULE_PARM_DESC(device_name, "Device name prefix"); /* Boolean parameter */static bool enable_dma = true;module_param(enable_dma, bool, 0644);MODULE_PARM_DESC(enable_dma, "Enable DMA transfers"); /* Array parameter */static int irq_list[4] = { -1, -1, -1, -1 };static int irq_count;module_param_array(irq_list, int, &irq_count, 0644);MODULE_PARM_DESC(irq_list, "IRQ numbers for devices"); /* Named parameter (different from variable name) */static int hw_timeout = 1000;module_param_named(timeout, hw_timeout, int, 0644);MODULE_PARM_DESC(timeout, "Hardware timeout in milliseconds"); static int __init mydriver_init(void){ pr_info("Driver loading with parameters:\n"); pr_info(" debug=%d\n", debug); pr_info(" buffer_size=%u\n", buffer_size); pr_info(" device_name=%s\n", device_name); pr_info(" enable_dma=%d\n", enable_dma); pr_info(" irq_count=%d\n", irq_count); pr_info(" hw_timeout=%d\n", hw_timeout); return 0;}Parameter Types:
| Type | C Type | Format |
|---|---|---|
| byte | unsigned char | 8-bit unsigned |
| short | short | 16-bit signed |
| ushort | unsigned short | 16-bit unsigned |
| int | int | 32-bit signed |
| uint | unsigned int | 32-bit unsigned |
| long | long | Pointer-sized signed |
| ulong | unsigned long | Pointer-sized unsigned |
| bool | bool | true/false, 1/0, y/n |
| charp | char * | String (char pointer) |
Permission Bits (mode):
0: Not visible in sysfs0444: Read-only0644: Root can write, others read0664: Owner/group write, others read12345678910111213141516171819202122232425
# Load with parameters on command linesudo insmod mydriver.ko debug=1 buffer_size=8192 device_name="custom" # Using modprobe (params in modprobe.conf or command line)sudo modprobe mydriver debug=1 # View current parameter values (if permission bits allow)cat /sys/module/mydriver/parameters/debugcat /sys/module/mydriver/parameters/buffer_size # Modify parameter at runtime (if writable)echo 1 > /sys/module/mydriver/parameters/debug # View all parameters of a modulels /sys/module/mydriver/parameters/ # Set default parameters in modprobe configuration# /etc/modprobe.d/mydriver.conf:options mydriver debug=1 buffer_size=8192 # View parameter info from module filemodinfo -p mydriver.ko# Output:# debug:Enable debug logging (0=off, 1=on) (int)# buffer_size:DMA buffer size in bytes (uint)Writable parameters (0644, 0666) can be changed at runtime by root. Your driver must handle this safely! Either make parameters read-only (0444), or implement proper synchronization and validation. A user modifying buffer_size while DMA is active could cause corruption.
Modern Linux systems automatically load drivers when hardware is detected. This magic is orchestrated by udev, the device manager daemon, using modalias strings that describe hardware in a way modules can match.
The Automatic Loading Flow:
uevent containing modaliasmodprobe <modalias>modules.alias database for matching modules12345678910111213141516171819202122232425262728
# PCI device modalias format:# pci:vVVVVVVVVdDDDDDDDDsvSSSSSSSSSsdSSSSSSSSSbcBCscSCi II# v = vendor, d = device, sv = subsystem vendor, sd = subsystem device# bc = base class, sc = subclass, i = interface # Example: Intel network controllerpci:v00008086d000010D3sv00001028sd000002A4bc02sc00i00 # USB device modalias format:# usb:vVVVVpPPPPdDDDDdcDCdscDSCdpDPicICiscISCipIPinIN# v = vendor, p = product, d = device version, dc = device class... # Example: USB mass storage deviceusb:v1234p5678d0100dc00dsc00dp00ic08isc06ip50 # Device tree (for embedded/ARM):# of:N<name>T<type>C<compatible> # Example: Compatible string matchingof:Nmydev*Cvendor,my-device-v2 # ACPI:# acpi:ABCD1234: # VIEW device modalias:cat /sys/class/net/eth0/device/modaliascat /sys/bus/pci/devices/0000:00:1f.0/modaliascat /sys/bus/usb/devices/1-1/modalias1234567891011121314151617181920212223242526272829303132333435363738394041
/* PCI driver with device table */static const struct pci_device_id mydriver_pci_ids[] = { { PCI_DEVICE(0x8086, 0x10D3) }, /* Specific device */ { PCI_DEVICE(0x8086, 0x10D4) }, { PCI_DEVICE_CLASS(PCI_CLASS_NETWORK_ETHERNET, 0xFFFFFF) }, { } /* Terminator */};MODULE_DEVICE_TABLE(pci, mydriver_pci_ids); /* This macro generates: * MODULE_ALIAS("pci:v00008086d000010D3sv*sd*bc*sc*i*"); * MODULE_ALIAS("pci:v00008086d000010D4sv*sd*bc*sc*i*"); * etc. * * These aliases are stored in the .modinfo section and * extracted by depmod into modules.alias */ /* USB driver with device table */static const struct usb_device_id mydriver_usb_ids[] = { { USB_DEVICE(0x1234, 0x5678) }, /* Vendor/Product */ { USB_INTERFACE_INFO(USB_CLASS_HID, 1, 2) }, /* Class match */ { }};MODULE_DEVICE_TABLE(usb, mydriver_usb_ids); /* Platform driver (device tree) */static const struct of_device_id mydriver_of_match[] = { { .compatible = "vendor,my-device" }, { .compatible = "vendor,my-device-v2" }, { }};MODULE_DEVICE_TABLE(of, mydriver_of_match); /* ACPI driver */static const struct acpi_device_id mydriver_acpi_ids[] = { { "VND1234", 0 }, { "PNP0A08", 0 }, { }};MODULE_DEVICE_TABLE(acpi, mydriver_acpi_ids);The modules.alias Database:
# After installing modules, update the database:
sudo depmod -a
# View generated aliases:
cat /lib/modules/$(uname -r)/modules.alias | grep mydriver
# alias pci:v00008086d000010D3sv*sd*bc*sc*i* mydriver
# alias pci:v00008086d000010D4sv*sd*bc*sc*i* mydriver
# Test which module a modalias would load:
modprobe --resolve-alias pci:v00008086d000010D3sv00001028sd000002A4bc02sc00i00
# mydriver
depmod scans all installed modules, extracts MODULE_ALIAS entries, and builds the modules.alias and modules.dep databases. Run depmod after installing new modules!
If automatic loading fails: 1) Check modalias matches with cat /sys/.../modalias and grep /lib/modules/.../modules.alias. 2) Verify depmod was run after module install. 3) Check udev rules don't block loading. 4) Use udevadm monitor to see events. 5) Check dmesg for loading errors.
Modules often depend on other modules for functionality. A USB storage driver needs the USB core module. A filesystem driver needs the block layer support. Module dependencies describe these relationships, allowing modprobe to load prerequisites automatically.
Types of Dependencies:
123456789101112131415161718192021
# View module dependenciesmodinfo mydriver | grep depends# depends: usb-storage,scsi_mod # View the dependency databasecat /lib/modules/$(uname -r)/modules.dep | grep mydriver# kernel/drivers/mydriver.ko: kernel/drivers/usb-storage.ko kernel/drivers/scsi/scsi_mod.ko # Show what modules would be loaded and in what ordermodprobe --show-depends mydriver# insmod /lib/modules/.../kernel/drivers/scsi/scsi_mod.ko# insmod /lib/modules/.../kernel/drivers/usb/storage/usb-storage.ko# insmod /lib/modules/.../kernel/drivers/mydriver.ko # View what modules are keeping another loadedlsmod | grep usb_storage# usb_storage 77824 1 mydriver <-- Used by mydriver # View reference countscat /sys/module/usb_storage/refcnt# 11234567891011121314151617181920212223242526272829303132333435363738394041
/* Soft dependency: optional but beneficial * * Module works without the dependency, but if dependency * is available, additional features are enabled. */ /* In module code - declare soft dependency */MODULE_SOFTDEP("pre: crypto_engine"); /* This tells modprobe to try loading crypto_engine before us, * but don't fail if it's not available. */ /* Runtime feature detection based on optional dependency */#include <linux/crypto.h> static bool have_hardware_crypto; static int __init mydriver_init(void){ struct crypto_alg *alg; /* Try to find optional crypto accelerator */ alg = crypto_find_alg("xts(aes)", NULL, 0, 0); if (!IS_ERR(alg)) { have_hardware_crypto = true; pr_info("Hardware crypto acceleration available\n"); } else { have_hardware_crypto = false; pr_info("Using software crypto fallback\n"); } /* Module functions fine either way */ return 0;} /* ================================================ */ /* In modprobe.conf (/etc/modprobe.d/mydriver.conf) */# softdep mydriver pre: optional_module# softdep mydriver post: cleanup_moduleReference Counting:
Modules track how many things are using them:
/* In driver - prevent module unload during operations */
static int mydev_open(struct inode *inode, struct file *file)
{
/* Try to increment this module's reference count */
if (!try_module_get(THIS_MODULE))
return -ENODEV;
/* ... open processing ... */
return 0;
}
static int mydev_release(struct inode *inode, struct file *file)
{
/* Decrement reference count */
module_put(THIS_MODULE);
return 0;
}
try_module_get() may fail if the module is being unloaded. Always check the return value.
Circular dependencies (A depends on B, B depends on A) are impossible to resolve—neither module can load first. The kernel will fail with symbol resolution errors. Design your modules to avoid circular dependencies, possibly by refactoring shared functionality into a third module.
Loading arbitrary kernel code is a significant security risk. A malicious module has full kernel privileges. Module signing provides cryptographic verification that modules haven't been tampered with and come from a trusted source.
Security Mechanisms:
| Config Option | Effect | Security Level |
|---|---|---|
| CONFIG_MODULE_SIG | Enables signature checking | Basic - can still load unsigned |
| CONFIG_MODULE_SIG_FORCE | Reject unsigned modules | Strong - unsigned fails |
| CONFIG_MODULE_SIG_ALL | Sign all in-tree modules at build | Development convenience |
| CONFIG_LOCK_DOWN_KERNEL | Prevent various security-sensitive ops | Maximum security |
| CONFIG_SECURITY_LOADPIN | Allow modules only from same filesystem | Specialized security |
123456789101112131415161718192021222324252627282930
# Generate signing keys (if not provided by distribution)./scripts/generate_signing_key.sh # Keys are stored in:# certs/signing_key.pem (private key - KEEP SECURE)# certs/signing_key.x509 (public cert - built into kernel) # Sign a module manually./scripts/sign-file sha256 certs/signing_key.pem \ certs/signing_key.x509 mydriver.ko # Verify signaturemodinfo mydriver.ko | grep sig# sig_id: PKCS#7# signer: Build time autogenerated kernel key# sig_key: 48:C4:A5:...# sig_hashalgo: sha256 # Check if kernel requires signaturescat /sys/module/module/parameters/sig_enforce# 0 = allow unsigned, 1 = require signatures # View kernel's trusted keyscat /proc/keys | grep builtin # Load signed module (same as unsigned on permissive systems)sudo insmod mydriver.ko # Error when unsigned module rejected:# insmod: ERROR: could not insert module: Required key not availableSecure Boot and Module Signing:
When Secure Boot is enabled:
This creates a complete chain of trust from firmware to kernel modules.
Managing Keys:
# Enroll a new signing key (requires MOK password at boot)
sudo mokutil --import my_key.der
# List enrolled keys
mokutil --list-enrolled
# For development: temporarily disable Secure Boot
# (In UEFI/ BIOS settings - not recommended for production)
DKMS (Dynamic Kernel Module Support) can integrate with MOK to sign modules automatically during kernel updates. Configure DKMS to use your enrolled key for seamless third-party driver support with Secure Boot enabled.
We've explored the complete journey of how device drivers are loaded into the kernel. Let's consolidate the key takeaways:
What's Next:
With loading understood, we'll explore driver bugs—the common mistakes that cause crashes, hangs, and data corruption. We'll examine debugging techniques, common pitfalls, and how to write robust code that avoids these issues.
You now understand how device drivers are loaded into the kernel—from manual insmod to automatic udev-triggered loading. This knowledge is essential for system administration, driver development, and debugging loading issues.