In Part 1 you understood the theory behind kernel modules. Now it is time to actually write one. By the end of this part, you will have compiled a working kernel module, loaded it into a running Linux 6.x kernel, and verified its output in the kernel log.
1. Anatomy of a Minimal Kernel Module
Every kernel module — no matter how complex — has two mandatory ingredients: an init function called when the module loads, and an exit function called when it unloads. There are also mandatory license and metadata declarations.
Here is the simplest possible kernel module, explained line by line:
/*
* mymodule.c — Minimal Linux Kernel Module
* Tested on Linux 6.x
*/
#include <linux/module.h> /* Required for all kernel modules */
#include <linux/init.h> /* Provides __init and __exit macros */
#include <linux/printk.h> /* Provides pr_info() and friends */
/* Module metadata — appears in modinfo output */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A minimal kernel module example");
MODULE_VERSION("1.0");
/*
* my_module_init — called when module is loaded
* Must return 0 on success, negative errno on failure.
* __init tells the kernel it can free this function's memory after boot.
*/
static int __init my_module_init(void)
{
pr_info("mymodule: loaded successfully\n");
return 0; /* 0 = success; any nonzero value aborts loading */
}
/*
* my_module_exit — called when module is unloaded
* Must clean up everything allocated in init.
* __exit marks this as cleanup code — not needed if module is built-in.
*/
static void __exit my_module_exit(void)
{
pr_info("mymodule: unloaded\n");
}
/* Register the init and exit functions with the kernel */
module_init(my_module_init);
module_exit(my_module_exit);
2. Understanding Every Line
#include <linux/module.h>This is the most important header for any kernel module. It provides the module_init() and module_exit() macros, the MODULE_* metadata macros, and the exported kernel symbol infrastructure. Without this, your file is not a kernel module.
#include <linux/init.h>Provides the __init and __exit section attributes. These are important memory optimization hints to the kernel (explained below).
#include <linux/printk.h>Provides pr_info(), pr_err(), pr_warn() and the full family of kernel logging functions. In modern Linux kernel code (6.x), prefer these pr_*() wrappers over calling printk() directly — they are cleaner and support pr_fmt() prefixing.
MODULE_LICENSE("GPL")This is not optional. It tells the kernel what license governs your module. Setting it to "GPL" allows your module to use GPL-only exported symbols — functions that are marked EXPORT_SYMBOL_GPL(). Many important kernel subsystem functions are GPL-only. Setting any non-GPL license restricts which kernel internals you can call, and the kernel will emit a “tainted kernel” warning.
Valid values: "GPL", "GPL v2", "GPL and additional rights", "Dual MIT/GPL", "Dual BSD/GPL", "Proprietary".
__init macroThis is a section attribute that places the init function into a special .init.text section in the binary. After the module finishes its initialization (or after the kernel boots for built-in modules), the kernel frees this memory. This means your init function’s code does not stay in RAM after it has done its job. Use __init on your init function and __initdata on any data that is only needed during initialization.
__exit macroPlaces the exit function in .exit.text. For modules that are compiled statically into the kernel (=y), there is no way to unload them, so the exit function is never called. The kernel can therefore discard it entirely for built-in modules. For loadable modules, __exit is preserved normally so it can be called at rmmod time.
module_init() and module_exit()These macros register your init and exit functions with the kernel module framework. You can name your functions anything you like — as long as you pass the correct function pointer here. The kernel calls the registered init function when the module is inserted and the exit function when it is removed.
3. The Return Value of the Init Function
The init function must return an integer. Return 0 on success. Return a negative error code (-ENOMEM, -ENODEV, -EINVAL, etc.) if initialization fails. When you return a nonzero value, the kernel aborts the load — insmod will print an error and the module will not be inserted.
static int __init my_module_init(void)
{
void *buf;
buf = kmalloc(1024, GFP_KERNEL); /* allocate 1KB kernel memory */
if (!buf) {
pr_err("mymodule: failed to allocate memory\n");
return -ENOMEM; /* abort loading — insmod will report an error */
}
pr_info("mymodule: initialized, buffer at %p\n", buf);
return 0;
}
static void __exit my_module_exit(void)
{
/* In real code: kfree(buf) here */
pr_info("mymodule: cleaned up\n");
}
init must be freed in exit. If init allocates memory, registers a device, or creates a file in /proc, then exit must do the corresponding cleanup — even if init failed partway through. Forgetting this causes kernel memory leaks that persist until the next reboot.
4. Writing the Module Makefile
Kernel modules are not compiled like regular C programs with gcc mymodule.c -o mymodule. They must be compiled using the kernel build system (kbuild), which sets up all the right compiler flags, header paths, and section attributes automatically.
Create a file named Makefile in the same folder as your mymodule.c:
# Makefile for a simple out-of-tree kernel module
# obj-m tells kbuild to build mymodule.c as a loadable module (.ko)
obj-m += mymodule.o
# Path to the kernel build directory for the currently running kernel
KDIR := /lib/modules/$(shell uname -r)/build
# Current working directory
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
$(MAKE) in a Makefile must be a real Tab character, not spaces. If you use spaces, make will fail with a confusing “missing separator” error. Every text editor has a “insert tab” option — make sure you use it here.
Your source directorymymodule.c + Makefile
|
→ |
make -C /lib/modules/…/build M=$(PWD) kbuild enters kernel build directory, compiles M= as a module |
→ |
mymodule.ko Ready to load with insmod |
|
Side outputs: mymodule.o · mymodule.mod.c · mymodule.mod.o · Module.symvers · modules.order · .tmp_versions/
|
||||
5. Building and Testing Your Module
With mymodule.c and Makefile in the same folder, follow these steps:
make
# Expected output (Linux 6.x):
# make -C /lib/modules/6.8.0-45-generic/build M=/home/user/mymodule modules
# CC [M] /home/user/mymodule/mymodule.o
# MODPOST /home/user/mymodule/Module.symvers
# CC [M] /home/user/mymodule/mymodule.mod.o
# LD [M] /home/user/mymodule/mymodule.ko
# BTF [M] /home/user/mymodule/mymodule.ko
ls -lh mymodule.ko
modinfo mymodule.ko
# Expected modinfo output:
# filename: /home/user/mymodule/mymodule.ko
# description: A minimal kernel module example
# author: Your Name
# license: GPL
# version: 1.0
# vermagic: 6.8.0-45-generic SMP preempt mod_unload
sudo insmod mymodule.ko
# No output means success (errors go to stderr)
# Check the kernel log:
dmesg | tail -5
# Expected:
# [12345.678901] mymodule: loaded successfully
lsmod | grep mymodule
# Expected:
# mymodule 16384 0
# (name, size in bytes, use count)
sudo rmmod mymodule
dmesg | tail -5
# Expected:
# [12350.123456] mymodule: unloaded
make clean
# Removes: *.ko, *.o, *.mod.c, *.mod.o, Module.symvers,
# modules.order, .tmp_versions/
6. Passing Parameters to a Module
You can pass configuration values to a module at load time using module_param(). This is how device drivers accept runtime configuration like IRQ numbers, I/O port addresses, or debug levels.
#include <linux/module.h>
#include <linux/init.h>
#include <linux/printk.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Module parameter example");
/* Declare a parameter: name, type, permissions in /sys/module/ */
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Set debug verbosity level (0=off, 1=basic, 2=verbose)");
static char *device_name = "mydevice";
module_param(device_name, charp, 0444);
MODULE_PARM_DESC(device_name, "Name of the target device");
static int __init param_module_init(void)
{
pr_info("param_module: loaded — debug_level=%d, device_name=%s\n",
debug_level, device_name);
return 0;
}
static void __exit param_module_exit(void)
{
pr_info("param_module: unloaded\n");
}
module_init(param_module_init);
module_exit(param_module_exit);
To pass parameters at load time:
# Pass values using insmod:
sudo insmod param_module.ko debug_level=2 device_name=uart0
# Kernel log will show:
# param_module: loaded — debug_level=2, device_name=uart0
# Parameters also appear in sysfs while the module is loaded:
cat /sys/module/param_module/parameters/debug_level
# Output: 2
# If permission is 0644, you can change it at runtime:
echo 1 | sudo tee /sys/module/param_module/parameters/debug_level
| Permission Value | Meaning | Use Case |
|---|---|---|
0 |
No sysfs entry created | Internal-only, not user-visible |
0444 |
Readable by all, not writable | Read-only config (device names, IDs) |
0644 |
Root can write, all can read | Runtime-tunable (debug levels, thresholds) |
0600 |
Root only read/write | Sensitive parameters |
7. Supported Parameter Types
/* Integer types */
static int my_int; module_param(my_int, int, 0644);
static long my_long; module_param(my_long, long, 0644);
static short my_short; module_param(my_short, short, 0644);
/* Unsigned types */
static uint my_uint; module_param(my_uint, uint, 0644);
static ulong my_ulong; module_param(my_ulong, ulong, 0644);
/* Boolean: accepts 0/1, y/n, Y/N */
static bool my_bool; module_param(my_bool, bool, 0644);
/* String (char pointer) */
static char *my_str; module_param(my_str, charp, 0444);
/* Arrays of integers */
static int my_arr[4];
static int my_arr_cnt;
module_param_array(my_arr, int, &my_arr_cnt, 0644);
/* Load example: */
/* sudo insmod mymod.ko my_arr=10,20,30,40 */
/* my_arr_cnt will be set to 4 automatically */
8. Module Init/Exit Lifecycle in Detail
|
📥 sudo insmod mymodule.ko
|
⇄ |
📤 sudo rmmod mymodule
|
9. Common Errors and How to Fix Them
Cause: The module was compiled against a different kernel version than what is running. The vermagic check failed.
Fix: Recompile your module. Run make clean then make again while the target kernel is booted. Check that uname -r output matches the path in your Makefile.
Cause: Your module uses a function that the kernel does not export, or a prerequisite module is not loaded.
Fix: Check that the function is exported with EXPORT_SYMBOL() in kernel source. If it is in another module (e.g., i2c-core), load that module first. The depends line in modinfo output shows required modules.
Cause: Something holds a reference to the module (an open file descriptor, another module depending on it, etc.).
Fix: Close any open device files, unload dependent modules first, or check lsmod to see which module holds the dependency.
Cause: The indentation in your Makefile uses spaces instead of a Tab character.
Fix: Replace all leading spaces before $(MAKE) with a single Tab character.
🎯 Interview Questions — Writing Kernel Modules
✅ Part 2 Summary
- Every module needs an init function, an exit function, and MODULE_LICENSE
- Use
__initand__exitfor memory efficiency - The init function must return 0 on success, negative errno on failure
- Modules are built using the kernel’s kbuild system — never plain gcc
- The Makefile’s
obj-mvariable tells kbuild what to compile as a module - Use
module_param()to accept configuration values at load time - Always free in exit() everything you allocated in init()
In Part 3, you will master kernel logging with printk and the pr_*() family, understand log levels, use rate-limited logging, and build a production-quality Makefile.
→ Continue to Part 3: printk, Logging & Makefile← Back to Part 1
EmbeddedPathashala — Free Linux Kernel Programming Course | Updated for Linux 6.x
