$linuxjunkies
>

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.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 1, 2026

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:

  • __init and __exit are macros that place the functions in special ELF sections. The kernel frees __init memory after boot, saving RAM.
  • printk is the kernel's printf. Always prefix messages with a log level such as KERN_INFO or KERN_ERR so the syslog priority is set correctly.
  • current is a per-CPU pointer to the task_struct of the running process — useful for debugging context.
  • Declaring the license as GPL is 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 -r and 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 in cat -A Makefile and look for ^I (tab) versus spaces.
  • Module loads but no dmesg output — your console log level may be below KERN_INFO. Run sudo dmesg -n 7 and 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 add CONFIG_DEBUG_INFO_BTF=n to your build config. For a simple hello-world module you can safely ignore this warning if the .ko is produced.
tested on:Ubuntu 24.04Fedora 40Arch rolling-2025-05Debian 12

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