Write Your First Kernel Module
Build, load, and debug a Linux kernel module from scratch: hello-world source, kbuild Makefile, insmod/modprobe workflow, and dmesg debugging on modern distros.
Before you start
- ▸Root or sudo access on a physical or virtual Linux machine
- ▸Kernel headers matching the running kernel (verified via /lib/modules/$(uname -r)/build)
- ▸gcc and make installed
- ▸Basic familiarity with C and the Linux command line
Writing a kernel module lets you run code in ring 0 — the highest privilege level on x86 — without recompiling the entire kernel. It is the foundation for custom drivers, performance instrumentation, and low-level hardware experiments. This guide takes you from a blank file to a loaded, debuggable module using only the tools already present on any modern Linux development workstation.
Prerequisites and Environment Setup
You need the kernel headers matching your running kernel, a C compiler, and make. The commands differ slightly by distro.
Debian / Ubuntu
sudo apt install build-essential linux-headers-$(uname -r)
Fedora / RHEL / Rocky
sudo dnf install kernel-devel kernel-headers gcc make
On RHEL and Rocky you may also need the elfutils-libelf-devel package if the build system complains about BTF generation.
Arch Linux
sudo pacman -S base-devel linux-headers
Verify the headers are present and match your running kernel before proceeding:
ls /lib/modules/$(uname -r)/build
If that directory is missing, your headers package did not install correctly. Do not continue until it exists.
Writing the Module Source
Create a working directory and open a new file called hello.c.
mkdir ~/hello_module && cd ~/hello_module
nano hello.c
Paste the following source. Every line is explained in the comments.
cat > hello.c << 'EOF'
#include /* module_init, module_exit */
#include /* MODULE_LICENSE and friends */
#include /* KERN_INFO, printk */
MODULE_LICENSE("GPL"); /* required; non-GPL taints the kernel */
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Minimal hello-world kernel module");
MODULE_VERSION("0.1");
static int __init hello_init(void)
{
printk(KERN_INFO "hello_module: loaded. pid=%d\n", current->pid);
return 0; /* non-zero return means init failed; module will not load */
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "hello_module: unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
EOF
A few things worth noting:
__initand__exitare macros that place the functions in special ELF sections. The kernel frees__initmemory after boot, saving RAM.printkis the kernel's printf. Always prefix messages with a log level such asKERN_INFOorKERN_ERRso the syslog priority is set correctly.currentis a per-CPU pointer to thetask_structof the running process — useful for debugging context.- Declaring the license as
GPLis not just formality; without it, certain exported kernel symbols are unavailable to your module.
Writing the Makefile
The kernel build system (kbuild) uses a specific Makefile convention. Create Makefile in the same directory — capitalisation matters.
cat > Makefile << 'EOF'
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
EOF
Important: the indented lines inside all and clean must use a real tab character, not spaces. Most terminal editors insert tabs by default when you use the heredoc above, but double-check if you edit the file manually.
obj-m tells kbuild to build hello.c as a loadable module (.ko file) rather than linking it directly into the kernel image.
Building the Module
make
Successful output looks roughly like:
make -C /lib/modules/6.8.0-45-generic/build M=/home/user/hello_module modules
CC [M] /home/user/hello_module/hello.o
MODPOST /home/user/hello_module/Module.symvers
CC [M] /home/user/hello_module/hello.mod.o
LD [M] /home/user/hello_module/hello.ko
BTF [M] /home/user/hello_module/hello.ko
The important output file is hello.ko. Inspect its metadata before loading:
modinfo hello.ko
You should see the author, description, version, and vermagic strings you set in the source.
Loading, Verifying, and Unloading
Load with insmod
sudo insmod hello.ko
insmod loads a module directly from a path. It does not resolve dependencies; use it during development when the .ko file is not yet installed system-wide.
Verify it loaded
lsmod | grep hello
You should see a line like hello 16384 0. The third column is the use count.
Check kernel messages with dmesg
sudo dmesg | tail -5
Look for your hello_module: loaded. line. On busy systems it may scroll past; filter for it explicitly:
sudo dmesg | grep hello_module
For live monitoring while you load and unload, run this in a second terminal:
sudo dmesg --follow
Unload the module
sudo rmmod hello
Note you pass the module name (without .ko) to rmmod. Check dmesg again to confirm the exit message appeared.
Installing with modprobe
modprobe is the production tool for managing modules. It reads /lib/modules/$(uname -r)/, resolves dependencies, and honours configuration in /etc/modprobe.d/. To use it with your module, install it into the module tree first.
sudo cp hello.ko /lib/modules/$(uname -r)/extra/
sudo depmod -a
depmod rebuilds the dependency and alias maps. After that:
sudo modprobe hello
sudo modprobe -r hello # unload
To load the module automatically at boot, create a drop-in file:
echo "hello" | sudo tee /etc/modules-load.d/hello.conf
systemd's systemd-modules-load.service reads files in that directory at boot and calls modprobe on each listed name.
Debugging Techniques with dmesg
Log levels
Kernel log levels run from 0 (KERN_EMERG) to 7 (KERN_DEBUG). During development, use KERN_DEBUG for verbose output and raise the console log level so messages appear immediately on the terminal:
sudo dmesg -n 8 # print everything including KERN_DEBUG to console
pr_* helpers
Modern kernel code uses the pr_info(), pr_err(), pr_debug() family instead of raw printk. They prepend the module name automatically and are easier to read:
cat > hello.c << 'EOF'
#include
#include
MODULE_LICENSE("GPL");
static int __init hello_init(void)
{
pr_info("loaded on CPU %d\n", smp_processor_id());
return 0;
}
static void __exit hello_exit(void)
{
pr_info("unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
EOF
Timestamps and monotonic clock
Pass -T to dmesg to convert monotonic timestamps to human-readable wall-clock times (accuracy is approximate):
sudo dmesg -T | grep hello
Troubleshooting
- insmod: ERROR: could not insert module hello.ko: Invalid module format — the kernel headers do not match the running kernel. Run
uname -rand confirm the headers package version is identical. - make: *** No rule to make target 'modules' — the Makefile has spaces instead of tabs before the
$(MAKE)invocation. Open the file incat -A Makefileand look for^I(tab) versus spaces. - Module loads but no dmesg output — your console log level may be below
KERN_INFO. Runsudo dmesg -n 7and reload the module. - Kernel taint warning after loading — you used a non-GPL license string, or the module was built against a different kernel version. A tainted kernel is still usable for development but some bug reports will be ignored by upstream.
- BTF: .tmp_vmlinux.btf: pahole (pahole) is not available — install
pahole(apt install dwarves/dnf install dwarves) or addCONFIG_DEBUG_INFO_BTF=nto your build config. For a simple hello-world module you can safely ignore this warning if the.kois produced.
Frequently asked questions
- Why does my module taint the kernel, and does it matter?
- A taint flag is set when a module lacks a GPL-compatible license, was built for a different kernel version, or triggers certain safety checks. For development it is harmless, but upstream kernel developers will ignore bug reports from tainted kernels. Declare MODULE_LICENSE("GPL") to avoid the most common taint.
- Can I use printk from any context, including interrupt handlers?
- Yes. printk is safe to call from interrupt context, NMI handlers, and atomic sections because it writes to an in-memory ring buffer. However, if the console log level is high it may attempt to flush to a serial console, which can introduce latency in timing-sensitive code.
- What happens if my module's init function returns a non-zero value?
- The kernel treats any non-zero return from the init function as an error, aborts the load, and insmod prints an error message. The exit function is not called in this case, so you must free any resources allocated before the failure point within the init function itself.
- How do I pass parameters to my module at load time?
- Declare a module parameter with module_param(name, type, permissions) and MODULE_PARM_DESC(). Then pass it as sudo insmod hello.ko myparam=42 or in /etc/modprobe.d/ as options hello myparam=42 for modprobe.
- Do I need Secure Boot disabled to load out-of-tree modules?
- On systems with Secure Boot enforced, the kernel requires modules to be signed with a key trusted by the firmware. You can either enroll your own Machine Owner Key (MOK) using mokutil and sign the module with scripts/sign-file from the kernel source, or disable Secure Boot in the UEFI settings for development purposes.
Related guides
AI and Artificial-Life Tools on Linux
Set up open-source AI/ML and artificial-life toolkits on Linux: PyTorch, JAX, DEAP, Avida, NetLogo, and RL environments with GPU driver guidance.
Assembly Language on Linux: A Starter Guide
Write x86-64 assembly on Linux from scratch: install NASM and GAS, learn syscalls, assemble and link a working program, then inspect and debug it.
How to Benchmark Disk Performance with fio
Learn to benchmark Linux disk performance with fio: writing job files, testing latency and throughput, and interpreting IOPS and percentile output correctly.
The Linux Boot Process Explained
Trace the full Linux boot sequence from UEFI firmware through GRUB2, the kernel, initramfs, and systemd to your login prompt — with diagnostics at each stage.