Writing a kernel module is the closest most developers get to operating system internals without becoming full-time kernel developers. This guide walks through a real module from boilerplate to a useful procfs interface.
Warning: Kernel modules run in ring 0. A bug can crash the entire system. Always test in a VM.
1. Prerequisites
# Debian/Ubuntu
sudo apt install build-essential linux-headers-$(uname -r)
# Verify
ls /lib/modules/$(uname -r)/build
2. Hello World Module
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel! Module loaded.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel! Module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");
3. The Makefile
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Build and test:
make
sudo insmod hello.ko
lsmod | grep hello
dmesg | tail -3
sudo rmmod hello
dmesg | tail -3
4. Passing Parameters
#include <linux/moduleparam.h>
static int count = 1;
static char *name = "world";
module_param(count, int, 0);
module_param(name, charp, 0);
static int __init hello_init(void)
{
int i;
for (i = 0; i < count; i++)
printk(KERN_INFO "Hello, %s! (#%d)\n", name, i);
return 0;
}
sudo insmod hello.ko count=3 name=engineer
dmesg | tail -3
5. Creating a /proc File
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/jiffies.h>
#define PROC_NAME "my_module_info"
static struct proc_dir_entry *proc_entry;
static ssize_t proc_read(struct file *filp, char __user *buf,
size_t count, loff_t *offp)
{
char kernel_buf[256];
int len;
if (*offp > 0)
return 0;
len = snprintf(kernel_buf, sizeof(kernel_buf),
"Uptime (jiffies): %llu\n"
"Module loaded: %s\n",
(unsigned long long)get_seconds(),
"active");
if (copy_to_user(buf, kernel_buf, len))
return -EFAULT;
*offp = len;
return len;
}
static const struct proc_ops proc_fops = {
.proc_read = proc_read,
};
static int __init hello_init(void)
{
proc_entry = proc_create(PROC_NAME, 0444, NULL, &proc_fops);
if (!proc_entry) {
printk(KERN_ERR "Failed to create /proc/%s\n", PROC_NAME);
return -ENOMEM;
}
printk(KERN_INFO "Module loaded. See /proc/%s\n", PROC_NAME);
return 0;
}
static void __exit hello_exit(void)
{
remove_proc_entry(PROC_NAME, NULL);
printk(KERN_INFO "Module unloaded.\n");
}
Test:
sudo insmod hello.ko
cat /proc/my_module_info
sudo rmmod hello
6. Debugging Kernel Modules
dmesg -w # follow mode — live kernel messages
lsmod # list loaded modules
modinfo hello.ko # module metadata
7. Kernel vs Userspace
| Aspect | Userspace | Kernel |
|---|---|---|
| Memory | malloc/free | kmalloc/kfree |
| printf | printf | printk (dmesg) |
| Error handling | errno | Negative errno codes |
| Floating point | Allowed | Prohibited |
| Sleep | Always allowed | Depends on context |
| Stack size | MB | 4KB-16KB |
Top 5 mistakes:
- Accessing userspace pointers directly — use
copy_from_user/copy_to_user - Sleeping in interrupt context — use
GFP_ATOMIC - Not checking return values — allocations and proc_create can fail
- Buffer overflows — kernel stack is 4-16KB, use kmalloc
- Race conditions — use spinlocks or mutexes
Start with hello-world, add procfs, then explore netfilter, character devices, or USB drivers. The kernel is not as inaccessible as it seems.