Loading a kernel module might look instantaneous from the outside — you run insmod or modprobe, and a second later, new functionality is available. But underneath that simplicity is a genuinely intricate sequence of steps the kernel walks through to safely bring new code into the most privileged execution context on the machine. I want to walk through exactly what happens during that initialization process, because understanding it makes you a much better systems programmer, whether you’re writing drivers or just debugging a system that won’t boot cleanly.
What Is a Kernel Module, Quickly Recapping
A kernel module is a piece of code that can be dynamically loaded into a running kernel, extending its functionality without requiring a reboot or a kernel recompile. On Linux, these are .ko (kernel object) files. Common uses include device drivers, filesystem implementations, and network protocol handlers — anything that needs to run with kernel privileges but doesn’t need to be compiled into the core kernel image permanently.
Step 1: Compilation and Object Format
Before initialization can happen at all, a kernel module needs to be built correctly. On Linux, this means compiling against the exact kernel headers matching the running kernel version, producing a .ko file — which is, under the hood, a specially formatted ELF (Executable and Linkable Format) object file containing not just compiled code, but also metadata sections the kernel’s module loader needs: a table of exported and required symbols, licensing information, version magic strings, and module parameters.
The version magic string deserves a special mention — it encodes the exact kernel version, compiler version, and configuration options the module was built against. If this doesn’t match the running kernel precisely, the load will fail immediately with a version mismatch error, a deliberate safety mechanism to prevent loading incompatible binary code into kernel space.
Step 2: The Loading Request
Initialization begins when something requests the module be loaded. This can happen a few ways:
- Explicit user command: Running
insmod modulename.kodirectly, or the more commonly usedmodprobe modulename, which additionally resolves and loads any dependency modules automatically based on dependency information generated bydepmod. - Automatic loading via udev/kmod: When new hardware is detected, the kernel’s device subsystem can trigger automatic module loading through the kernel’s request_module() mechanism, which is how plug-and-play device support actually works under the hood.
- Boot-time loading: Modules listed in configuration files like
/etc/modules-load.d/are loaded automatically during system startup.
Step 3: The System Call Boundary
When you run insmod, the userspace tool doesn’t do the actual loading itself — it reads the .ko file into memory and then invokes the init_module() (or the more modern finit_module()) system call, passing the module image (or a file descriptor, for finit_module()) to the kernel. This is the critical transition point: from here forward, the kernel itself takes full control of the loading process.
Step 4: Verification and Signature Checking
On systems with module signing enabled (a common security hardening measure, especially relevant for Secure Boot environments), the kernel verifies the module’s cryptographic signature against a set of trusted keys before proceeding any further. If the module isn’t signed, or the signature doesn’t validate, the kernel refuses to load it — assuming strict signature enforcement is configured (CONFIG_MODULE_SIG_FORCE). This closes off a significant attack vector where malicious code could otherwise be loaded directly into kernel space.
The kernel also checks the module’s license tag (MODULE_LICENSE()) at this stage. Modules that don’t declare a GPL-compatible license are restricted from calling certain kernel-internal (non-exported-for-proprietary-use) symbols — this is Linux’s way of enforcing a soft boundary around its GPL licensing without literally blocking proprietary modules from loading at all.
Step 5: Symbol Resolution
This is one of the more technically interesting steps. The module’s ELF object contains a list of external symbols it needs — kernel functions and variables it calls or references but doesn’t define itself. The kernel’s module loader walks through this list and resolves each symbol against the kernel’s own exported symbol table (built from every EXPORT_SYMBOL() and EXPORT_SYMBOL_GPL() declaration throughout the kernel source, plus the exported symbols of any already-loaded modules the new module depends on).
If any required symbol can’t be found — perhaps because a dependency module isn’t loaded yet, or because the symbol simply doesn’t exist in this kernel build — the load fails at this stage with an “unknown symbol” error. This is exactly why modprobe is generally preferred over raw insmod: it consults dependency information ahead of time and loads prerequisite modules in the correct order automatically.
Step 6: Memory Allocation and Relocation
Once symbols are resolved, the kernel allocates memory for the module — separate regions for executable code, read-only data, and writable data, each with appropriate memory protection flags (a security-relevant detail; you generally don’t want your module’s code section to also be writable, to reduce the attack surface for code injection). The module’s code is then relocated: since it wasn’t necessarily compiled to run at a predetermined memory address, the loader patches up addresses within the code to reflect wherever the module actually ended up in kernel memory.
Step 7: Running the Module’s Init Function
This is the step most people think of as “initialization” — and it is, but as you can see, it’s really just one step in a longer chain. Every Linux kernel module defines an initialization function, conventionally registered via the module_init() macro:
static int __init my_module_init(void)
{
pr_info("my_module: initializing\n");
// Register a device, allocate resources, set up data structures, etc.
return 0; // 0 indicates success; a negative errno value indicates failure
}
module_init(my_module_init);
The kernel calls this function once, synchronously, as the final step of loading. Inside it, a module typically does things like: registering itself with a relevant kernel subsystem (as a character device, a network protocol handler, a filesystem type, etc.), allocating any memory or data structures it needs going forward, setting up interrupt handlers if it’s a device driver, and creating entries under /proc or /sys for userspace interaction if relevant.
Critically, if this init function returns a non-zero (error) value, the kernel treats the whole load as a failure — it unwinds whatever partial setup occurred (calling cleanup as needed) and returns an error to the calling userspace tool, rather than leaving a half-initialized module resident in the kernel.
Step 8: Marking the Module as Live
Once the init function returns successfully, the kernel marks the module’s state as “live” in its internal module list (visible to userspace via /proc/modules and the lsmod command), making its exported symbols available for any other modules that might depend on it going forward, and incrementing appropriate reference counts.
A Complete Example, Start to Finish
Let’s trace a realistic scenario: you plug in a USB Wi-Fi adapter.
- The kernel’s USB subsystem detects a new device via a hardware interrupt and enumerates it, reading its vendor and product ID.
- The kernel’s hotplug mechanism (via udev) matches this ID against a modules.alias database and determines which driver module handles this specific hardware.
udevinvokesmodprobefor the matching module (say,rtl8188eufor a Realtek chipset).modprobechecks module dependencies, loading any prerequisite modules (likecfg80211, the generic wireless configuration API module) first, each going through the full load sequence described above.- The main driver module loads: signature is verified, symbols are resolved against the kernel and against
cfg80211‘s exported symbols, memory is allocated, code is relocated. - The module’s init function runs, registering the device with the kernel’s wireless subsystem, setting up interrupt handlers for the USB device’s data transfers, and creating a new network interface (like
wlan0). - The module is now live, and your Wi-Fi adapter is ready to use — all of this typically happens within a fraction of a second of plugging in the device.
The __init and __initdata Markers
You may have noticed the __init marker attached to the example init function shown earlier. This is a compiler/linker annotation with a genuinely useful purpose: it places the function’s code into a special section of the kernel image reserved specifically for initialization-only code. For modules that are built directly into the kernel (rather than loaded dynamically as .ko files), the kernel can safely discard this entire section of memory after boot completes, since init functions only ever run once and are never needed again — freeing up that memory for general use. A companion annotation, __initdata, does the same for data structures only needed during initialization.
For dynamically loaded modules specifically, this discarding behavior doesn’t apply in quite the same way (since the whole module, including its init function, gets freed together as a unit once loading — including running the init function — completes, rather than needing this section to be discarded separately). Still, following the convention of marking init-only code and data this way remains good practice, both for consistency with how the code would behave if built statically into the kernel, and because it clearly signals to other developers reading the code exactly which parts are meant to run exactly once during setup versus which parts are part of the module’s ongoing, persistent functionality.
Common Initialization Failures and What They Mean
- “Invalid module format”: Almost always a version mismatch — the module was built against a different kernel version than the one currently running.
- “Unknown symbol in module”: A required dependency module isn’t loaded, or the running kernel’s configuration doesn’t export a symbol the module needs.
- “Operation not permitted”: Often indicates a signature verification failure on a system enforcing signed modules, or insufficient privileges (loading modules requires root/
CAP_SYS_MODULEcapability). - Init function returns an error: The module’s own init logic detected a problem — commonly, a hardware resource conflict, failed memory allocation, or a device that isn’t actually present despite matching an ID.
Module Parameters During Initialization
One detail worth exploring further: modules often accept configurable parameters at load time, declared in source using module_param():
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Verbosity of debug logging (0-3)");
The third argument sets the permissions on a corresponding entry the kernel creates under /sys/module/modulename/parameters/, controlling whether userspace can read and/or modify the value after loading. During Step 7 (running the init function) described above, these parameter values — supplied at load time via insmod modulename.ko debug_level=2 — are already populated into the module’s variables before the init function’s first line executes, since the kernel parses and applies them as part of the loading sequence, immediately before invoking module_init(). This lets a single compiled module adapt its behavior without requiring a rebuild for different deployment scenarios, which is genuinely useful for things like adjustable logging verbosity, buffer sizes, or feature toggles in production drivers.
Initialization Order and Dependency Chains
When multiple related modules load together — the common driver-stack scenario described earlier — initialization order matters enormously and is enforced automatically by the symbol resolution process itself. A module cannot successfully complete loading (and therefore cannot have its init function invoked) until every module it depends on has already finished its own init function and published its exported symbols. This creates a natural, enforced dependency chain: lower-level infrastructure modules (say, a generic bus driver) always finish initializing before the higher-level modules that build on them (say, a specific device driver using that bus).
This ordering guarantee is genuinely important for correctness. If a Wi-Fi hardware driver’s init function tried to register itself with the generic wireless configuration subsystem before that subsystem module had finished its own setup, the registration call would either fail outright or, worse, operate against a partially initialized data structure. The kernel’s dependency-driven load ordering eliminates this entire class of bug by construction, rather than requiring each module to defensively check whether its dependencies are “ready.”
Deferred and Asynchronous Initialization
Not every driver can complete all its setup synchronously within module_init() — some hardware genuinely takes time to respond (waiting for a device to come out of reset, for instance), and blocking the entire module-loading sequence (and potentially the boot process, if the module loads during early boot) for that delay is undesirable. The kernel offers mechanisms for this: a module’s init function can kick off a background kernel thread, schedule deferred work via a workqueue, or in some cases use the kernel’s asynchronous probing infrastructure to complete slower hardware bring-up after module_init() itself has already returned. This keeps the overall boot/load sequence fast while still correctly handling hardware with longer initialization requirements, at the cost of the module needing to carefully track and expose its own “not yet fully ready” state to anything that might try to use it in that window.
Windows and macOS for Comparison
Windows drivers go through a broadly analogous process when loaded — the Plug and Play Manager and the I/O Manager cooperate to load a driver’s .sys file, verify its digital signature (mandatory for kernel-mode drivers on 64-bit Windows since Vista), resolve its imports against the kernel and other loaded drivers, and then call its DriverEntry() routine — functionally equivalent to Linux’s module_init() function.
macOS historically used kernel extensions (kexts) with a similar load-and-initialize sequence, though Apple has been steadily deprecating third-party kexts in favor of user-space DriverKit extensions specifically to avoid the stability and security risks associated with third-party code running in kernel space at all.
Best Practices for Writing Module Init Functions
- Keep init functions focused and fast — don’t perform long-running or blocking operations that could stall the boot process or a manual load command.
- Always check every allocation and registration call for failure, and unwind cleanly (freeing anything already allocated) if a later step fails partway through.
- Use
pr_info()/pr_err()logging generously during development to make failures easy to diagnose viadmesg. - Declare an accurate
MODULE_LICENSE(),MODULE_AUTHOR(), andMODULE_DESCRIPTION()— these aren’t just cosmetic; the license tag has real functional implications for symbol visibility. - Test module loading and unloading repeatedly during development (load, unload, load again) to catch resource leaks or double-registration bugs early.
Summary
Kernel module initialization is a multi-stage process that starts long before your module_init() function ever runs — compilation, the system call boundary, signature verification, symbol resolution, and memory setup all happen first, each a checkpoint where a misconfigured or malicious module can be rejected. Understanding this full pipeline, not just the init function itself, is essential for anyone debugging module load failures or writing kernel-level code professionally.
FAQs
What’s the difference between insmod and modprobe? insmod loads a single module file directly with no dependency resolution — if it needs symbols from another unloaded module, it simply fails. modprobe is smarter: it consults dependency metadata (built by depmod) and automatically loads any prerequisite modules first, in the correct order.
Can a kernel module initialization process fail safely? Yes — if the init function returns an error code, the kernel treats the load as failed, and (assuming the module’s cleanup logic is written correctly) unwinds any partial setup rather than leaving the kernel in an inconsistent state.
Why do kernel modules need to match kernel versions so strictly? Because kernel modules are compiled binary code that directly manipulates kernel-internal data structures, and those structures’ exact memory layout can change between kernel versions/configurations. A mismatch risks memory corruption, so the kernel refuses to load mismatched modules rather than risk instability.
Does module signing slow down the loading process significantly? No, cryptographic signature verification is extremely fast (typically a few milliseconds at most) compared to the overall loading process, and it’s a one-time cost per load rather than an ongoing performance tax.
Can I load a kernel module without root privileges? No, loading kernel modules requires the CAP_SYS_MODULE capability, which is normally only granted to the root user, precisely because kernel modules run with unrestricted kernel privileges and represent a significant security boundary.
Official References
- Linux Kernel Module Programming Guide: https://www.kernel.org/doc/html/latest/kbuild/modules.html
- Linux Module Signing Documentation: https://www.kernel.org/doc/html/latest/admin-guide/module-signing.html
- Microsoft Windows Driver Development Documentation: https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/