How to Write a Custom Linux Kernel Module Step by Step: Hello World, Makefile, and insmod

How to Write a Custom Linux Kernel Module Step by Step: Hello World, Makefile, and insmod

by | Oct 14, 2026 | Uncategorized | 0 comments

If you want to write a Linux kernel module but every guide you find either dumps 40 pages of theory on you or skips the part where the build actually fails, this tutorial is for you. We start from an empty directory and finish with a loadable .ko file that prints to the kernel log, plus the real error messages you will hit on Ubuntu and Debian and how to fix each one.

Everything below was run on Ubuntu 24.04 LTS / 26.04 LTS and Debian 13 (trixie) with kernels in the 6.8 to 6.17 range. The same commands work on almost any modern distribution, only the package names change.

What you will build in this tutorial

  1. A minimal hello.c module with an init function and an exit function.
  2. A kbuild Makefile that compiles it against your running kernel.
  3. Loading and unloading it with insmod, rmmod, lsmod and dmesg.
  4. A second version that accepts a module parameter.
  5. Module signing so it loads on a machine with Secure Boot enabled.

Kernel module basics (the 2 minute version)

A Linux kernel module is a piece of object code that can be loaded into and unloaded from the running kernel on demand, without rebooting and without recompiling the whole kernel. Most drivers are modules. Filesystems, network protocols and security hooks can also be modules.

Writing kernel code is not the same as writing a normal C program. Keep this table in mind before you type a single line:

Aspect User space program Kernel module
Entry point main() module_init() function
C library glibc, any library you link No libc. Only kernel APIs
Output printf() to stdout pr_info() / printk() to the kernel ring buffer
Floating point Allowed Avoid it
A bug means Segfault, one process dies Oops or panic, the machine may freeze
Memory malloc() / free() kmalloc() / kfree()

Strong advice: do this in a virtual machine (VirtualBox, QEMU/KVM, multipass) or on a spare box. A bad module can hang your system, and you will experiment a lot.

linux terminal code

Step 1: Install the build tools and kernel headers

To compile an out-of-tree module you need the headers and build infrastructure of the exact kernel you are running. You do not need the full kernel source tree.

On Ubuntu

sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)

On Debian

sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)
# if that package is not found on Debian, install the meta package:
sudo apt install linux-headers-amd64

Verify that the build directory exists

uname -r
ls -l /lib/modules/$(uname -r)/build

You should see a symlink pointing into /usr/src/linux-headers-.... If ls says No such file or directory, the headers are missing or they do not match your running kernel (very common right after a kernel upgrade when you have not rebooted). Reboot, then reinstall the header package for the new version. The piece Building External Modules makes a good next read.

Step 2: Create the project directory

mkdir -p ~/kmod-hello
cd ~/kmod-hello

Two files will live here: hello.c and Makefile. That is all a minimal module needs.

linux terminal code

Step 3: Write the module source (hello.c)

Create hello.c with your editor (nano hello.c) and paste this:

#include <linux/init.h>      /* __init and __exit macros */
#include <linux/module.h>    /* core module macros: module_init, MODULE_LICENSE */
#include <linux/kernel.h>    /* pr_info and friends */

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <[email protected]>");
MODULE_DESCRIPTION("A minimal Hello World Linux kernel module");
MODULE_VERSION("1.0");

static int __init hello_init(void)
{
        pr_info("hello: module loaded, kernel says hi\n");
        return 0;   /* 0 means success. Any negative value aborts loading */
}

static void __exit hello_exit(void)
{
        pr_info("hello: module unloaded, cleaning up\n");
}

module_init(hello_init);
module_exit(hello_exit);

Line by line: what each piece actually does

  • The three headers are the classic dependency trio. linux/module.h pulls in the module machinery, linux/init.h gives you the section markers, linux/kernel.h gives you logging helpers. Kernel headers are always included with angle brackets and a linux/ prefix, they are resolved from the kernel build tree, never from /usr/include.
  • MODULE_LICENSE("GPL") is not decoration. Without a GPL compatible license string the kernel marks itself tainted and refuses you access to a large number of GPL-only symbols. Expect a module license 'unspecified' taints kernel warning if you forget it.
  • __init tells the kernel that the function is only needed once at load time, so its memory can be freed afterwards.
  • __exit marks cleanup code that can be dropped entirely if the module is built statically into the kernel.
  • Return value of the init function: 0 for success, or a negative errno such as -ENOMEM or -ENODEV to make insmod fail with a meaningful error.
  • pr_info() is the modern shorthand for printk(KERN_INFO ...). Always end the string with \n, otherwise your line may get merged with the next one.
  • module_init() / module_exit() register the two callbacks. The function names are yours to choose, the macros are what matter.

Kernel log levels cheat sheet

Macro Old form Use it for
pr_emerg() KERN_EMERG System is unusable
pr_alert() KERN_ALERT Action required immediately
pr_err() KERN_ERR Error conditions, failed probe
pr_warn() KERN_WARNING Something suspicious
pr_notice() KERN_NOTICE Normal but significant
pr_info() KERN_INFO Load / unload messages
pr_debug() KERN_DEBUG Debug output (needs DEBUG or dynamic debug)

Step 4: Write the kbuild Makefile

You do not call gcc yourself. You ask the kernel build system (kbuild) to build your object for you. Create a file named exactly 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

.PHONY: all clean

Critical detail: the two indented lines must start with a real TAB character, not spaces. If you get Makefile:6: *** missing separator. Stop., that is exactly what happened. In nano, press Ctrl+V then Tab if your editor expands tabs, or run sed -i 's/^ /\t/' Makefile.

What each line means

  • obj-m += hello.o: build hello.o as a loadable module (m for module). Kbuild deduces that it must compile hello.c.
  • -C $(KDIR): change into the kernel build tree so kbuild’s own rules and configuration are used.
  • M=$(PWD): come back to this directory to find the sources. This is the standard out-of-tree (external module) invocation documented in the kernel’s Building External Modules page.
  • modules target: produce the .ko file.

Multiple source files in one module? Use this pattern instead:

obj-m += mydrv.o
mydrv-objs := main.o helpers.o sysfs.o

Step 5: Build the module

make

Expected output looks like this:

make -C /lib/modules/6.12.0-31-generic/build M=/home/you/kmod-hello modules
make[1]: Entering directory '/usr/src/linux-headers-6.12.0-31-generic'
  CC [M]  /home/you/kmod-hello/hello.o
  MODPOST /home/you/kmod-hello/Module.symvers
  CC [M]  /home/you/kmod-hello/hello.mod.o
  LD [M]  /home/you/kmod-hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.12.0-31-generic'

Now list the directory with ls -l:

File Role
hello.ko The loadable module. This is what you feed to insmod
hello.o Compiled object of your C file
hello.mod.c Generated glue: version magic, license, aliases
Module.symvers Exported symbol versions of this build
modules.order Build order bookkeeping

Inspect the metadata before loading

modinfo ./hello.ko
filename:       /home/you/kmod-hello/hello.ko
version:        1.0
description:    A minimal Hello World Linux kernel module
author:         Your Name <[email protected]>
license:        GPL
vermagic:       6.12.0-31-generic SMP preempt mod_unload modversions

The vermagic line must match your running kernel. If it does not, loading will fail with Invalid module format.

linux terminal code

Step 6: Load the module with insmod and read dmesg

Open a second terminal and start watching the kernel log live:

sudo dmesg -wH

Back in the first terminal:

sudo insmod ./hello.ko

Silence from insmod means success. The log terminal shows:

[  +0.000012] hello: module loaded, kernel says hi

Confirm the module is resident:

lsmod | grep hello
# hello                  12288  0

ls /sys/module/hello/
# coresize  holders  initsize  initstate  notes  refcnt  sections  taint  uevent

The third column of lsmod is the reference count. It must be 0 before you can unload.

insmod vs modprobe

  • insmod ./hello.ko loads one exact file by path and does not resolve dependencies. Perfect for development.
  • modprobe hello loads by module name from /lib/modules/$(uname -r)/ and pulls in dependencies automatically. It only works after installation:
sudo make -C /lib/modules/$(uname -r)/build M=$PWD modules_install
sudo depmod -a
sudo modprobe hello

Step 7: Unload the module with rmmod

sudo rmmod hello
# or: sudo modprobe -r hello

The log shows:

[  +0.000009] hello: module unloaded, cleaning up

Note that rmmod takes the module name (hello), not the file name (hello.ko). If you get Module hello is in use, something still holds a reference: check lsmod holders and close any open file descriptors on your device.

Step 8: Add a module parameter

Real modules are configurable at load time. Extend hello.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hello World module with parameters");

static char *name = "world";
static int count = 1;

module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "Who to greet");

module_param(count, int, 0644);
MODULE_PARM_DESC(count, "How many times to greet");

static int __init hello_init(void)
{
        int i;

        if (count < 1 || count > 10) {
                pr_err("hello: count must be between 1 and 10\n");
                return -EINVAL;      /* insmod will fail: Invalid argument */
        }

        for (i = 0; i < count; i++)
                pr_info("hello: hello, %s! (%d/%d)\n", name, i + 1, count);

        return 0;
}

static void __exit hello_exit(void)
{
        pr_info("hello: goodbye, %s\n", name);
}

module_init(hello_init);
module_exit(hello_exit);

Rebuild and load with arguments:

make clean && make
sudo insmod ./hello.ko name=linuxfuture count=3
sudo dmesg | tail -3
cat /sys/module/hello/parameters/name
sudo rmmod hello

The 0644 permission argument creates a readable and writable entry under /sys/module/hello/parameters/. Use 0 if you do not want the parameter exposed in sysfs at all.

linux terminal code

Step 9: Secure Boot and unsigned module rejection

This is where most beginners on Ubuntu give up. If Secure Boot is enabled with lockdown enforced, the kernel will not load your freshly built module:

sudo insmod ./hello.ko
# insmod: ERROR: could not insert module hello.ko: Key was rejected by service

And in dmesg:

Loading of unsigned module is rejected
Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7

Check your state first:

mokutil --sb-state
# SecureBoot enabled

You have three options.

Option A: sign your module with your own MOK key (recommended)

# 1. Generate a signing key pair (valid for a long period)
mkdir -p ~/mok && cd ~/mok
openssl req -new -x509 -newkey rsa:2048 -nodes -days 36500 \
  -keyout MOK.priv -outform DER -out MOK.der \
  -subj "/CN=Local kernel module signing key/"

# 2. Enroll the public key in the firmware (you will set a one time password)
sudo mokutil --import MOK.der

# 3. Reboot and choose "Enroll MOK" -> Continue -> Yes, then enter that password
sudo reboot

After the reboot, sign the module every time you rebuild it:

cd ~/kmod-hello
sudo /usr/src/linux-headers-$(uname -r)/scripts/sign-file \
  sha256 ~/mok/MOK.priv ~/mok/MOK.der hello.ko

modinfo hello.ko | grep -i sig
sudo insmod ./hello.ko

You can add the signing step straight into your Makefile as a sign: target so it never gets forgotten.

Option B: disable Secure Boot validation

sudo mokutil --disable-validation
# set a password, reboot, choose "Change Secure Boot state", confirm

Quick, but it weakens the boot chain of that machine. Fine for a throwaway lab VM, not for your work laptop.

Option C: develop in a VM without Secure Boot

The cleanest path for learning. A QEMU/KVM or VirtualBox guest with Secure Boot off loads unsigned modules instantly, and a kernel panic costs you a reboot instead of a lost work session.

Common build and load errors, and how to fix them

Message Cause Fix
make: *** /lib/modules/6.x.x/build: No such file or directory Kernel headers not installed or not matching the running kernel sudo apt install linux-headers-$(uname -r) then reboot if you just upgraded the kernel
fatal error: linux/module.h: No such file or directory You tried to compile with plain gcc instead of kbuild Always build through make -C /lib/modules/$(uname -r)/build M=$PWD modules
Makefile:6: *** missing separator. Stop. Spaces instead of a TAB before the recipe line Replace leading spaces with a real tab character
insmod: ERROR: could not insert module: Invalid module format vermagic mismatch, module built for another kernel Compare modinfo hello.ko \| grep vermagic with uname -r, then make clean && make
Key was rejected by service Unsigned module under Secure Boot Sign with a MOK key (Option A above) or disable validation
insmod: ERROR: could not insert module: Operation not permitted Missing root privileges, or kernel lockdown in integrity mode Use sudo, check cat /sys/kernel/security/lockdown
insmod: ERROR: could not insert module: File exists A module with that name is already loaded sudo rmmod hello first
module license 'unspecified' taints kernel No MODULE_LICENSE macro Add MODULE_LICENSE("GPL");
Unknown symbol in module You used a GPL-only or non exported symbol Check the license string, check /proc/kallsyms, verify the symbol is EXPORT_SYMBOLed in your kernel version
rmmod: ERROR: Module hello is in use Reference count above zero Close users of the device, check the holders column of lsmod
error: implicit declaration of function ... Kernel API changed between versions Guard with LINUX_VERSION_CODE / KERNEL_VERSION() checks or target one kernel series

Handy commands to keep in your notes

Command What it does
sudo insmod ./hello.ko Load a .ko file directly
sudo rmmod hello Unload by module name
lsmod List loaded modules with refcounts (reads /proc/modules)
modinfo ./hello.ko Show license, params, vermagic, signature
sudo dmesg -wH Follow the kernel log with human timestamps
sudo dmesg -C Clear the ring buffer before a test run
sudo modprobe -r hello Remove a module and its unused dependencies
cat /proc/modules Raw module list with load addresses
journalctl -k -f Kernel messages through systemd, persistent across reboots
linux terminal code

Good habits before you write anything bigger

  • Prefix every log message with your module name, or better, define #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt before the includes and let the kernel do it for you.
  • Undo in reverse order. Whatever the init function allocates or registers, the exit function must release in the opposite order, and the init function must clean up its own partial work on error paths (the classic goto err_... ladder).
  • Never return 0 from init after a failure. A half initialized module that stays loaded is how you crash later.
  • Follow kernel coding style: tabs of 8, 80 to 100 column lines, no CamelCase. Run ./scripts/checkpatch.pl --no-tree -f hello.c from a kernel source tree.
  • Package it with DKMS once it is useful, so it is rebuilt automatically on every kernel upgrade instead of breaking silently.
  • Version guard your APIs. Internal kernel interfaces are not stable, code that built on 6.1 may not build on 6.17.

Where to go next

  1. A character device: register a major/minor with alloc_chrdev_region() or the simpler misc device API, implement open, read, write and release in a file_operations struct, and talk to your module with echo and cat.
  2. sysfs and procfs entries to expose state instead of parameters.
  3. Kernel threads, workqueues and timers for anything periodic.
  4. A platform or PCI driver skeleton with a probe and remove pair, the shape almost every real driver takes.
  5. Rust for Linux if you want to try the newer path. Rust module support has been in mainline since 6.1 and keeps growing, but C remains the language of the overwhelming majority of the tree.

FAQ

Do I need the full kernel source to write a Linux kernel module?

No. For an out-of-tree module you only need the matching kernel headers plus the build infrastructure, which is what linux-headers-$(uname -r) installs. You need the full source only if you want to modify the kernel itself or build in-tree. rockylinux.org walks through the specifics.

Why does insmod say “Invalid module format”?

Because the module’s vermagic does not match the running kernel. It typically happens after a kernel upgrade or when you build inside a container with different headers. Compare modinfo hello.ko | grep vermagic with uname -r, then rebuild with make clean && make.

What is the difference between insmod and modprobe?

insmod takes a path to a .ko file and loads exactly that, ignoring dependencies. modprobe takes a module name, searches /lib/modules/$(uname -r), reads modules.dep and loads dependencies first. Use insmod while developing, modprobe once the module is installed.

Can I write a kernel module in C++?

Not practically. The Linux kernel is written in C (with some assembly) and its build system, ABI and coding rules assume C. The only officially supported alternative today is Rust. tldp.org walks through the specifics.

Where do printk and pr_info messages go?

Into the kernel ring buffer, which you read with dmesg or journalctl -k. They may also land in /var/log/kern.log depending on your logging setup. Messages below the current console log level will not appear on a TTY, but they are still in the buffer.

How do I make my module load automatically at boot?

Install it (modules_install plus depmod -a), then add its name to a file in /etc/modules-load.d/, for example echo hello | sudo tee /etc/modules-load.d/hello.conf. Put any parameters in /etc/modprobe.d/hello.conf with an options hello name=linuxfuture line.

Is it safe to test kernel modules on my main machine?

Not really. A dereferenced null pointer in kernel space can produce an oops or a full panic, and a module that refuses to unload forces a reboot. Use a VM or a dedicated test machine, and take snapshots.

Why must the module be signed on Ubuntu?

Because with Secure Boot enabled the kernel runs in lockdown integrity mode and refuses code it cannot verify. Enrolling your own Machine Owner Key and signing the .ko with scripts/sign-file keeps Secure Boot intact while letting your own modules load.

Wrap up

Two files, three commands. That is genuinely all it takes to write a Linux kernel module, build it against your running kernel and see it print into dmesg. The hard part was never the hello world, it was the environment: matching headers, a Makefile with real tabs, and Secure Boot signing. With those out of the way, you can move on to character devices, sysfs attributes and real drivers with the same make, insmod, dmesg, rmmod loop you just learned.