Linux kernel module programming for 6.x and 7.x kernels

Updated November 2026

A kernel module is a piece of code you can load into the running Linux kernel and remove again, without rebooting. To write one you need the headers for your exact kernel, a C file with an init and an exit function, and a two-line Makefile that hands the work to the kernel's own build system, kbuild. This page builds a working module on a current 6.x or 7.x kernel (Debian 13 and RHEL 10 use 6.12, Ubuntu 26.04 LTS uses 7.0, as of November 2026) and covers loading, parameters, DKMS and Secure Boot signing.

The classic Linux Kernel Module Programming Guide is kept by the Linux Documentation Project at tldp.org, but it was written for 2.2 to 2.6 kernels and its examples no longer build. A maintained edition with working examples for recent 5.x and 6.x kernels lives at github.com/sysprog21/lkmpg and is readable online at sysprog21.github.io/lkmpg (book under OSL-3.0, example code under GPL-2.0). Use it after this page for character devices, /proc and sysfs files, timers, interrupts and more.

Use a virtual machine. Module code runs with full kernel privileges. A bad pointer does not end in a segmentation fault; it can freeze the machine or corrupt data. Experiment in a VM you can reset.

Install the build requirements

You need a compiler, make, and the kernel headers and build files that match the running kernel exactly:

# Debian, Ubuntu
sudo apt install build-essential kmod linux-headers-$(uname -r)

# Fedora, RHEL/Alma/Rocky
sudo dnf install gcc make kernel-devel-$(uname -r)

# Arch (for the default "linux" kernel)
sudo pacman -S base-devel linux-headers

Check that /lib/modules/$(uname -r)/build exists: it points to those build files. If you installed a kernel update but have not rebooted, uname -r still names the old kernel, and its headers may already be gone. Reboot first.

A first module

// SPDX-License-Identifier: GPL-2.0
/*
 * hello.c - a minimal loadable kernel module with one parameter
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/printk.h>

static char *whom = "world";
module_param(whom, charp, 0444);
MODULE_PARM_DESC(whom, "Who to greet (default: world)");

static int __init hello_init(void)
{
	pr_info("hello: Hello, %s!\n", whom);
	return 0;		/* non-zero would make the load fail */
}

static void __exit hello_exit(void)
{
	pr_info("hello: Goodbye, %s.\n", whom);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <you@example.org>");
MODULE_DESCRIPTION("Minimal example module");

What each part does:

The kbuild Makefile

Put this Makefile in the same directory. The command lines must start with a tab, not spaces:

obj-m := hello.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(CURDIR) modules

clean:
	$(MAKE) -C $(KDIR) M=$(CURDIR) clean

obj-m := hello.o tells kbuild to build hello.ko from hello.c. -C $(KDIR) runs the kernel's own Makefile, and M= tells it you are building an external module in this directory. For a module made of several files, add a line such as hello-y := main.o util.o. Using $(CURDIR) rather than $(PWD) keeps the Makefile working under sudo make.

$ make
make -C /lib/modules/6.12.113+deb13-amd64/build M=/home/you/hello modules
  CC [M]  /home/you/hello/hello.o
  MODPOST /home/you/hello/Module.symvers
  CC [M]  /home/you/hello/hello.mod.o
  LD [M]  /home/you/hello/hello.ko
$ modinfo ./hello.ko
description:    Minimal example module
license:        GPL
vermagic:       6.12.113+deb13-amd64 SMP preempt mod_unload modversions
parm:           whom:Who to greet (default: world) (charp)

The vermagic line records the exact kernel the module was built for. A module only loads into that kernel, so rebuild after every kernel update (or let DKMS do it, below). make clean removes the build files; make -C /lib/modules/$(uname -r)/build M=$PWD help lists the other targets.

Load, inspect and unload

sudo insmod ./hello.ko whom=Linux
sudo dmesg | tail -3                  # or: journalctl -k -n 3
lsmod | grep hello
cat /sys/module/hello/parameters/whom
sudo rmmod hello
sudo dmesg | tail -1

Expected kernel log lines: hello: Hello, Linux! after loading and hello: Goodbye, Linux. after removal. The parameter file in /sys/module/hello/parameters/ exists because the permission argument was 0444 (readable by all); with 0 there is no sysfs file. Other parameter types include int, uint, long, bool and arrays with module_param_array().

The first time you load your own module, the log also shows lines like "loading out-of-tree module taints kernel" and "module verification failed: signature and/or required key missing - tainting kernel". That is normal on a distribution kernel without Secure Boot: the module loads, and the kernel records the fact so that bug reports show a third-party module was present. cat /proc/sys/kernel/tainted shows the taint flags as a number.

Install it for modprobe

insmod takes a file path and does not load dependencies. modprobe loads by name from /lib/modules/$(uname -r) and handles dependencies and configuration:

sudo make -C /lib/modules/$(uname -r)/build M=$PWD modules_install
sudo depmod -a
sudo modprobe hello whom=Linux
sudo modprobe -r hello

External modules are installed into /lib/modules/$(uname -r)/updates/. On a distribution kernel, modules_install may print "SSL error … No such file or directory" from sign-file: the distribution's signing key is not shipped, so the module is installed unsigned. The build files on Debian also lack System.map, so run depmod -a yourself as shown. To set a parameter permanently, create /etc/modprobe.d/hello.conf with the line options hello whom=Linux; to load the module at every boot, put hello on a line in /etc/modules-load.d/hello.conf.

Rebuild automatically with DKMS

DKMS (Dynamic Kernel Module Support) rebuilds out-of-tree modules whenever a new kernel is installed; many packaged third-party drivers use it. Install the dkms package, copy your source to /usr/src/hello-0.1/, and add a dkms.conf there:

PACKAGE_NAME="hello"
PACKAGE_VERSION="0.1"
BUILT_MODULE_NAME[0]="hello"
DEST_MODULE_LOCATION[0]="/kernel/drivers/misc"
MAKE[0]="make KDIR=${kernel_source_dir}"
AUTOINSTALL="yes"
sudo dkms add hello/0.1
sudo dkms build hello/0.1
sudo dkms install hello/0.1
dkms status
sudo dkms remove hello/0.1 --all      # when you are done

${kernel_source_dir} is the build directory of the kernel DKMS is building for (by default /lib/modules/<version>/build), which is why the Makefile uses KDIR ?=. AUTOINSTALL="yes" makes DKMS rebuild the module for each new kernel. Fedora, RHEL, openSUSE and Ubuntu ignore DEST_MODULE_LOCATION and use their own directory.

Secure Boot and module signing

Many distribution kernels (Debian's, for example) switch on lockdown when UEFI Secure Boot is enabled, and then refuse unsigned modules. insmod reports "ERROR: could not insert module", and the kernel log says something like "Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7". Check the state with mokutil --sb-state.

You can turn Secure Boot off in the firmware for a test machine. To keep it on, create your own Machine Owner Key (MOK), enroll it once through the shim boot loader, and sign every module you build:

# 1. a key pair (keep MOK.priv secret: it can sign code your kernel will trust)
openssl req -new -x509 -newkey rsa:2048 -nodes -days 3650 \
  -subj "/CN=Local module signing key/" \
  -addext "extendedKeyUsage=codeSigning" \
  -keyout MOK.priv -outform DER -out MOK.der

# 2. ask shim to enroll the public key; choose a one-time password
sudo mokutil --import MOK.der
#    reboot, choose "Enroll MOK" in the blue MOK manager screen, enter the password

# 3. after the reboot: sign the module and load it
/lib/modules/$(uname -r)/build/scripts/sign-file sha256 MOK.priv MOK.der hello.ko
modinfo hello.ko | grep signer
sudo insmod ./hello.ko

Signing appends the signature to the file, so sign after every rebuild. mokutil --list-enrolled shows enrolled keys. DKMS does this for you: on first use it creates /var/lib/dkms/mok.key and /var/lib/dkms/mok.pub and signs every module it builds; you only enroll the certificate once with sudo mokutil --import /var/lib/dkms/mok.pub. Distributions can point DKMS at other key files (Ubuntu, for example, can use the key in /var/lib/shim-signed/mok/), so check mok_signing_key and mok_certificate in /etc/dkms/framework.conf and enroll the certificate named there.

Where to go next

Mistakes to avoid

Sources