Basic: Module Anatomy {:.gc-basic}
Basic
A Linux kernel module is a compiled object file that can be loaded into a running kernel without rebooting. The module infrastructure provides two mandatory hooks — module_init and module_exit — that the kernel calls when loading and unloading.
// hello.c — minimal kernel module
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <you@example.com>");
MODULE_DESCRIPTION("Hello World kernel module");
MODULE_VERSION("1.0");
static int __init hello_init(void)
{
pr_info("hello: module loaded\n");
return 0; /* non-zero means init failed */
}
static void __exit hello_exit(void)
{
pr_info("hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
The __init and __exit attributes place the functions in special ELF sections. After the module initialises successfully, __init memory is freed. __exit is discarded entirely for built-in code (modules compiled into the kernel) since it can never be unloaded.
Module metadata macros
| Macro | Purpose |
|---|---|
MODULE_LICENSE("GPL") |
Required — unlocks GPL-only symbols |
MODULE_AUTHOR(...) |
Displayed by modinfo |
MODULE_DESCRIPTION(...) |
One-line summary |
MODULE_VERSION(...) |
Arbitrary version string |
MODULE_ALIAS(...) |
Additional module aliases |
Out-of-tree Makefile
# Makefile — place alongside hello.c
obj-m += hello.o
# For a multi-file module:
# mydrv-objs := main.o helper.o
# obj-m += mydrv.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# Build, load, inspect, unload
make
sudo insmod hello.ko
dmesg | tail -3
lsmod | grep hello
modinfo hello.ko
sudo rmmod hello
lsmod reads /proc/modules. modinfo parses the .modinfo ELF section embedded during compilation.
Module Parameters {:.gc-basic}
Basic
Module parameters let you tune behaviour at load time without recompiling. They are declared with module_param() in the global scope.
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
static int irq_num = 17;
static bool verbose = false;
static char *dev_name = "mydev0";
static int counts[4];
static int ncounts;
module_param(irq_num, int, 0644);
MODULE_PARM_DESC(irq_num, "Hardware IRQ number (default 17)");
module_param(verbose, bool, 0444);
MODULE_PARM_DESC(verbose, "Enable verbose logging (default false)");
module_param(dev_name, charp, 0444);
MODULE_PARM_DESC(dev_name, "Device name string (default mydev0)");
module_param_array(counts, int, &ncounts, 0644);
MODULE_PARM_DESC(counts, "Array of up to 4 count values");
static int __init param_init(void)
{
int i;
pr_info("irq_num=%d verbose=%d dev_name=%s\n",
irq_num, verbose, dev_name);
for (i = 0; i < ncounts; i++)
pr_info(" counts[%d] = %d\n", i, counts[i]);
return 0;
}
module_init(param_init);
Supported types for module_param
| Type keyword | C type |
|---|---|
bool |
bool |
invbool |
bool (logic inverted) |
int |
int |
uint |
unsigned int |
short |
short |
ushort |
unsigned short |
long |
long |
ulong |
unsigned long |
charp |
char * (kernel copies string) |
hexint |
unsigned int (hex input accepted) |
The third argument to module_param is the sysfs permission bits. Setting 0 hides the parameter from sysfs entirely. When non-zero, the parameter appears at /sys/module/<name>/parameters/<param> and can be changed at runtime if the permission allows writes.
# Load with parameter overrides
sudo insmod param.ko irq_num=22 verbose=1 counts=10,20,30
# Read/write at runtime via sysfs
cat /sys/module/param/parameters/irq_num
echo 25 | sudo tee /sys/module/param/parameters/irq_num
Module Dependencies and modprobe {:.gc-mid}
Intermediate
insmod loads a single .ko file but does not resolve dependencies. modprobe consults the dependency database built by depmod.
# Rebuild the module dependency database (run as root after installing modules)
depmod -a
# depmod reads all .ko files under /lib/modules/$(uname -r)/
# and writes /lib/modules/$(uname -r)/modules.dep (text) and
# modules.dep.bin (binary, faster to parse)
# modprobe: loads module and all dependencies
sudo modprobe snd_hda_intel
# modprobe: remove module and unused dependencies
sudo modprobe -r snd_hda_intel
# insmod: load specific .ko, no dependency resolution
sudo insmod /path/to/mymodule.ko
# Show detailed module information
modinfo e1000e
modinfo output fields
filename: /lib/modules/6.1.0/kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko
description: Intel(R) PRO/1000 Network Driver
author: Intel Corporation
license: GPL v2
version: 3.8.7
alias: pci:v00008086d...
depends: ptp
retpoline: Y
intree: Y
vermagic: 6.1.0 SMP mod_unload
Autoloading configuration
# /etc/modules-load.d/mymodule.conf — load at boot
mymodule
# /etc/modprobe.d/mymodule.conf — set default parameters
options mymodule irq_num=22 verbose=1
# Blacklist a module (prevent autoload)
# /etc/modprobe.d/blacklist.conf
blacklist nouveau
# Soft dependency (load snd_pcm before snd_hda_intel if available)
softdep snd_hda_intel pre: snd_pcm
Dependency types
| Mechanism | File | Effect |
|---|---|---|
depends: |
modules.dep | Hard load-order dependency |
softdep pre: |
modprobe.d | Load listed modules before, if available |
softdep post: |
modprobe.d | Load listed modules after, if available |
alias |
modules.alias | Maps device IDs to module name |
Symbol Exports {:.gc-mid}
Intermediate
Modules exist in separate namespaces. To call a function defined in another module, the exporting module must explicitly publish the symbol using EXPORT_SYMBOL or EXPORT_SYMBOL_GPL.
/* exporter.c — publishes helper functions */
#include <linux/module.h>
#include <linux/export.h>
int mylib_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(mylib_add); /* available to all modules */
int mylib_secret(int x)
{
return x * 42;
}
EXPORT_SYMBOL_GPL(mylib_secret); /* GPL-licensed modules only */
static int __init exporter_init(void) { return 0; }
static void __exit exporter_exit(void) {}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
/* consumer.c — calls functions from exporter */
#include <linux/module.h>
#include <linux/kernel.h>
extern int mylib_add(int a, int b);
extern int mylib_secret(int x);
static int __init consumer_init(void)
{
pr_info("add(3,4) = %d\n", mylib_add(3, 4));
pr_info("secret(5) = %d\n", mylib_secret(5));
return 0;
}
module_init(consumer_init);
MODULE_LICENSE("GPL");
# Load exporter first, then consumer
sudo insmod exporter.ko
sudo insmod consumer.ko
# Verify exported symbols in the running kernel
grep mylib /proc/kallsyms
# All exported symbols from all modules
cat /proc/kallsyms | grep " T " # text (code) symbols
EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL
EXPORT_SYMBOL_GPL enforces that the consuming module declares MODULE_LICENSE("GPL") (or compatible). The kernel checks this at insmod time. Attempting to load a proprietary module that uses a GPL-only symbol results in:
ERROR: module consumer uses GPL-only symbols
Symbol versioning (CONFIG_MODVERSIONS)
When enabled, the kernel computes a CRC of each exported symbol’s type signature and embeds it in the module. A version mismatch between the running kernel’s symbol CRC and the module’s expected CRC causes load failure, preventing ABI breakage silently.
Advanced: Kernel Build System Integration {:.gc-adv}
Advanced
In-tree modules live under drivers/ (or sound/, net/, etc.) and are integrated with Kconfig and the kernel’s Makefile hierarchy.
# drivers/misc/mydev/Kconfig
config MYDEV
tristate "My custom device driver"
depends on HAS_IOMEM
select REGMAP_I2C
help
Say Y to build this driver into the kernel,
M to build as a loadable module, or N to skip.
# drivers/misc/mydev/Makefile
obj-$(CONFIG_MYDEV) += mydev.o
mydev-objs := main.o helper.o irq.o
# drivers/misc/Makefile — add one line
obj-$(CONFIG_MYDEV_DIR) += mydev/
Kconfig tristate values
| Value | Meaning |
|---|---|
y |
Built into vmlinux |
m |
Compiled as .ko loadable module |
n |
Not compiled at all |
Cross-compiling out-of-tree modules
# Makefile for ARM64 cross-compilation
ARCH ?= arm64
CROSS_COMPILE ?= aarch64-linux-gnu-
KDIR ?= /opt/linux-src
obj-m += mydrv.o
all:
$(MAKE) -C $(KDIR) M=$(PWD) \
ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
install:
$(MAKE) -C $(KDIR) M=$(PWD) \
ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) \
INSTALL_MOD_PATH=/opt/rootfs modules_install
DKMS (Dynamic Kernel Module Support)
DKMS automatically recompiles out-of-tree modules when a new kernel is installed — essential for distribution packages.
# /usr/src/mydrv-1.0/dkms.conf
PACKAGE_NAME="mydrv"
PACKAGE_VERSION="1.0"
BUILT_MODULE_NAME[0]="mydrv"
BUILT_MODULE_LOCATION[0]="."
DEST_MODULE_LOCATION[0]="/updates/dkms"
AUTOINSTALL="yes"
MAKE="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build"
CLEAN="make clean"
# Register and build
dkms add -m mydrv -v 1.0
dkms build -m mydrv -v 1.0 -k $(uname -r)
dkms install -m mydrv -v 1.0 -k $(uname -r)
Interview Q&A {:.gc-iq}
Interview Q&A
Q1: When exactly does module_init run relative to kernel boot?
For built-in code (obj-y), the __init functions are called by do_initcalls() during boot, ordered by initcall level (core_initcall, device_initcall, etc.). For loadable modules (.ko), module_init runs inside sys_init_module the moment insmod/modprobe loads the module into a running kernel — this can be any time after boot.
Q2: Why does EXPORT_SYMBOL_GPL actually enforce the GPL?
The kernel checks the license field embedded in the .modinfo section at load time. If a module calls a GPL-only symbol but declares a non-GPL license, load_module() rejects it with -EPERM. This is a legal enforcement mechanism — a proprietary module must not link against GPL internals.
Q3: Why does modprobe need modules.dep? Why can’t it just scan .ko files?
Scanning thousands of .ko files on every modprobe invocation would be too slow. depmod does this scan once at install time and writes modules.dep (a simple text makefile-style dependency graph). modprobe reads this cached file to determine load order in milliseconds.
Q4: What is the difference between insmod and modprobe?
insmod takes a filesystem path to a .ko file and loads it directly without resolving dependencies. If a required symbol is not present in the kernel, insmod fails. modprobe takes a module name, looks it up in modules.dep, loads all dependencies first, then loads the requested module. modprobe also reads /etc/modprobe.d/ for options and aliases.
Q5: What makes a kernel “tainted” and why does it matter?
A tainted kernel has had non-GPL or out-of-tree modules loaded, or has had other anomalous events (oops, unsigned module, etc.). The taint flags are visible in /proc/sys/kernel/tainted and printed in kernel crash messages. Kernel developers may refuse to debug issues on tainted kernels because the proprietary or out-of-tree code could be responsible.
Q6: How do you pass parameters to a module at load time?
# Via insmod (key=value after the .ko path)
sudo insmod mydrv.ko irq_num=22 verbose=1
# Via modprobe (reads /etc/modprobe.d/ or command line)
sudo modprobe mydrv irq_num=22 verbose=1
# Permanently via /etc/modprobe.d/mydrv.conf
echo "options mydrv irq_num=22 verbose=1" | sudo tee /etc/modprobe.d/mydrv.conf
Q7: What does vermagic mean in modinfo output?
vermagic is a string embedded in every module that encodes the kernel version, SMP configuration, preemption model, and other ABI-significant settings. The running kernel checks this string against its own at load time. A mismatch causes insmod to fail with invalid module format. You cannot load a module built for kernel 5.15 into kernel 6.1 without recompilation.
References {:.gc-ref}
References
| Resource | Link |
|---|---|
| Linux Kernel Module Programming Guide | https://sysprog21.github.io/lkmpg/ |
| kernel.org: kbuild modules documentation | https://www.kernel.org/doc/html/latest/kbuild/modules.html |
| kernel.org: kernel parameters | https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html |
| man 8 insmod | https://linux.die.net/man/8/insmod |
| man 8 modprobe | https://linux.die.net/man/8/modprobe |
| man 8 modinfo | https://linux.die.net/man/8/modinfo |
| man 8 depmod | https://linux.die.net/man/8/depmod |
| DKMS documentation | https://github.com/dell/dkms |
| Kernel Symbol Namespaces | https://www.kernel.org/doc/html/latest/core-api/symbol-namespaces.html |