R0KernelPath
module 096 / 262

Your First Kernel Module: Hello, World

Build, load, debug, and tear down a real Linux kernel module from scratch — no kernel recompile needed.

~6h focused study9 concepts

Overview

Every monumental system starts with a single foothold. In the Linux kernel, that foothold is the Loadable Kernel Module (LKM) — a chunk of object code that can be dynamically inserted into a running kernel, live, without rebooting, and removed just as cleanly when you are done. The hello-world LKM is simultaneously the simplest and most profound program you will ever write: it runs at ring 0 (the CPU's most privileged mode), shares the same address space as the scheduler, the memory allocator, and every driver on your machine, and can bring the whole system down with a single bad pointer. That is not a warning to scare you — it is an invitation to treat this carefully and feel the weight of the power you are picking up.

This module takes you from a blank terminal to a fully working LKM that you can load, inspect, parameterize, and remove. Along the way you will learn how the kernel's build system (Kbuild) actually works, what printk really is and why it is not printf, how the kernel's module loader links your object file into the live kernel, and how to navigate the build-test-debug loop that every kernel developer lives in. Every concept is grounded in real, runnable code that you can type in and try right now.

By the end of this module you will have a mental model of what a kernel module is at the ELF-object level, not just the "how to copy-paste" level. That model will serve you for everything that follows: device drivers, file systems, tracing infrastructure, and eventually patches to the mainline kernel itself. The hello-world module is the seed; everything else is the tree.

why this matters for the kernel

Kernel development is uniquely unforgiving: there is no operating system underneath you to catch mistakes, no memory-safe runtime to absorb a null dereference, and no debugger you can simply attach and single-step through (at least not without specialized hardware or KGDB setup). The LKM is the community's answer to making this approachable: you can write kernel code in an isolated translation unit, load it into a running VM, watch dmesg scroll by, and rmmod it when something goes wrong — all without touching the host kernel source tree or rebooting. This workflow is how the vast majority of driver development happens in industry and in the open-source community today.

For your path toward contributing to the mainline kernel, mastering the LKM workflow is not optional. Patch reviewers expect you to understand module init/exit lifecycle, MODULE_LICENSE, the difference between __init and non-init code, and what Kbuild does with your Makefile. If you want to write a driver, a file system, a tracer, or a custom syscall test — every single one of those starts here. The debugging muscle you build in this module (reading dmesg, interpreting kernel oops messages, using modinfo) is the same muscle you will use when bisecting a regression in a 30 million line codebase three years from now.

LKM Lifecycle: From Source File to Running Kernel Code

rendering diagram…

A .ko file is built in two phases by Kbuild, then loaded via a syscall that relocates symbols, checks version compatibility, and calls your init function at ring 0. rmmod reverses the process cleanly.

The concepts

01What is a Kernel Module? (The Big Picture)

The Linux kernel is a monolithic kernel, which means — at first glance — that all of the kernel's code runs in a single, enormous address space at ring 0. But monolithic does not mean static. Linux supports loadable kernel modules (LKMs): ELF shared-object files (.ko files — kernel object) that are linked into the running kernel image on demand.

Think of the kernel as a city. The city has a permanent skyline — the core kernel built at compile time (vmlinux). Modules are like prefabricated buildings that get craned in and bolted on while the city is fully occupied and running. When you no longer need a building, you crane it back out. The city never shuts down.

Technically: a .ko file is a relocatable ELF object (type ET_REL). It is not a self-contained executable. When you call insmod or modprobe, the kernel's module loader reads the ELF sections, performs kernel-space relocation (adjusting symbol references to match where the kernel's own symbols actually live in memory), allocates a region in the module memory area (historically near the kernel text in x86-64 to satisfy 32-bit RIP-relative calls), and then calls your init function. This is analogous to dlopen() in userspace, but with none of the safety nets.

Key consequences:

  • Your module code runs at ring 0 — the same privilege level as the scheduler. A bug in your module is a bug in the kernel.
  • Your module shares the kernel's virtual address space. You can call any exported kernel function directly.
  • There is no separate heap. malloc does not exist. You use kmalloc, vmalloc, or slab caches.
  • There is no page fault handler for kernel NULL dereferences (on most configs) — it is an immediate kernel panic or oops.
# Check if your kernel supports modules (almost always yes)
grep CONFIG_MODULES /boot/config-$(uname -r)
# Should print: CONFIG_MODULES=y

# See currently loaded modules
lsmod | head -20

# Inspect a .ko file as ELF (after you build one)
readelf -h hello.ko        # ELF type should be ET_REL
readelf -s hello.ko        # symbol table
readelf -S hello.ko        # section headers

02The Anatomy of a Hello-World Module

A minimal kernel module has exactly four required elements:

  1. #include guards — the kernel has its own header tree under include/ and arch/*/include/. You never include <stdio.h> or <stdlib.h>. Everything you need lives under <linux/...>.
  2. MODULE_LICENSE — this is not optional boilerplate. The kernel uses this macro to decide whether to mark itself as tainted when your module loads. A tainted kernel is a kernel that is running non-GPL code; many kernel developers will refuse to investigate a bug report from a tainted kernel. Use "GPL" (or "GPL v2", "Dual MIT/GPL", etc.).
  3. An init function tagged with module_init()** — called when the module is inserted. Returns 0 on success, negative errno on failure.
  4. An exit function tagged with module_exit()** — called when the module is removed. Returns void — by the time this runs, the module is already being torn down; there is no way to abort.

The __init and __exit annotations are important but subtle. __init tells the linker to place the function in the .init.text ELF section. For built-in kernel code (compiled directly into vmlinux, not a module), the kernel frees this section after boot — the init code is literally thrown away to reclaim memory. For modules, __init is less critical but still conventional; it signals intent and future-proofs your code if someone ever merges it into the core kernel.

The __exit annotation similarly goes into .exit.text. For built-in code, this section is never linked — there is no unloading a built-in. For modules it is retained and called on rmmod.

// hello.c — a complete, correct, minimal kernel module
#include <linux/init.h>    // __init, __exit macros
#include <linux/module.h>  // module_init, module_exit, MODULE_* macros
#include <linux/kernel.h>  // KERN_INFO, printk

MODULE_LICENSE("GPL");           // required; taints kernel if missing/non-GPL
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Hello, kernel world");
MODULE_VERSION("0.1");

static int __init hello_init(void)
{
    printk(KERN_INFO "hello: module loaded\n");
    return 0;  // 0 = success; negative errno = failure, module won't load
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "hello: module unloaded\n");
    // void return — no way to abort at this point
}

module_init(hello_init);
module_exit(hello_exit);

03The Kbuild System and Out-of-Tree Makefile

The Linux kernel does not use a plain Makefile — it uses Kbuild, a sophisticated recursive make infrastructure that has been evolving since before most readers of this module were born. When you build a module, you are hooking into Kbuild, not replacing it.

The critical insight: your Makefile does not actually do the compilation. Your Makefile tells Kbuild what object files make up your module, and then Kbuild — running inside the kernel source tree — handles all compiler flags, CFLAGS, ABI checks, and ELF magic.

Here is what the two-phase Makefile trick does:

  • Phase 1 (first make invocation): KERNELDIR is not set, so the all target runs. It calls make again, but this time changes directory to the kernel build tree ($(KERNELDIR)) and passes M=$(PWD) back. M tells Kbuild "look in this external directory for the module source".
  • Phase 2 (Kbuild running in the kernel tree): Kbuild reads your Makefile again, but this time KERNELDIR is set (it is the current directory), so it skips the outer branch and only sees the obj-m assignment. obj-m := hello.o means "build hello.o as a module object".

The obj-m syntax is Kbuild's object list. If your module had multiple source files (hello_core.c, hello_proc.c), you would write:

hello-objs := hello_core.o hello_proc.o
obj-m := hello.o

Kbuild links hello_core.o and hello_proc.o into hello.ko automatically.

The -C $(KERNELDIR) flag is essential: it gives Kbuild access to the kernel's .config, the generated headers (include/generated/), the module symbol map (Module.symvers), and all the ABI/version magic that ensures your module will actually load into this kernel and not some incompatible version.

# Makefile (place in same directory as hello.c)

# The kernel build directory. On a Debian/Ubuntu system this is
# /lib/modules/<kernel-version>/build, which is a symlink to the
# actual kernel headers/source used to build the running kernel.
KERNELDIR ?= /lib/modules/$(shell uname -r)/build

# obj-m tells Kbuild: build hello.o as a module (.ko)
obj-m := hello.o

# Default target: invoke Kbuild from the kernel tree,
# pointing M back at our source directory.
all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

# Clean: again delegate to Kbuild
clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

# Install the module into the running kernel's module tree
install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

04Building Out-of-Tree: Prerequisites and Build Output

"Out-of-tree" (OOT) means your module source lives outside the kernel source tree. This is the standard workflow for driver development and for learning: you do not need to download the full 30 GB kernel repository. You only need the kernel headers for the running kernel.

Prerequisites on Debian/Ubuntu:

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

This installs the headers package, which contains the Kbuild infrastructure and generated headers (but not all kernel source files). It is enough to build out-of-tree modules.

What make produces:

After a successful build you will see several files. The important ones are:

  • hello.ko — the final kernel object. This is what you load.
  • hello.o — the compiled object file before module-specific processing.
  • hello.mod.c — auto-generated C file that contains module metadata: the version magic string, the license, parameter descriptors, and symbol version checksums. You did not write this file — Kbuild generates it.
  • hello.mod.o — compiled version of hello.mod.c.
  • Module.symvers — a map of exported symbols and their CRC checksums. Critical when one module depends on another.
  • .hello.ko.cmd — a tiny file recording the exact compiler invocation. Useful for debugging build issues.

The version magic is the most important build artifact you cannot see directly. Run modinfo hello.ko and look at the vermagic field. It will look like 6.1.0-21-amd64 SMP preempt mod_unload modversions aarch64. This string must exactly match the running kernel's vermagic, or insmod will refuse to load the module with an Invalid module format error. This is a safety check: a module built against the 6.1 headers will have a different ABI than the 6.6 kernel, and loading it could corrupt data structures.

# Full build and inspection workflow

# 1. Install prerequisites (Debian/Ubuntu)
sudo apt install linux-headers-$(uname -r) build-essential

# 2. Build
make
# Output: hello.ko (among others)

# 3. Inspect the module metadata — no root needed
modinfo hello.ko
# filename:       /path/to/hello.ko
# description:    Hello, kernel world
# author:         Your Name
# license:        GPL
# version:        0.1
# vermagic:       6.1.0-21-amd64 SMP preempt mod_unload modversions x86_64

# 4. Check the ELF type (should be ET_REL, a relocatable object)
readelf -h hello.ko | grep Type
# Type: REL (Relocatable file)

# 5. See which kernel symbols your module references
nm hello.ko | grep " U "   # U = undefined, resolved at load time
# e.g.:  U printk
#        U __this_module

05insmod, modprobe, and the Module Loader

There are two userspace tools for loading modules: insmod and modprobe. They are not the same.

insmod path/to/hello.ko — the dumb loader. It reads the file you point it at, hands the raw ELF bytes to the init_module(2) system call, and the kernel does all the heavy lifting: relocation, symbol resolution, version magic check, and calling your init function. insmod has no awareness of dependencies between modules.

modprobe module_name — the smart loader. It reads /lib/modules/$(uname -r)/modules.dep (a dependency graph built by depmod) and automatically loads all prerequisite modules in the right order before loading yours. For production drivers you always use modprobe. For development you often use insmod directly because you have not yet run depmod to register your new module.

What happens inside the kernel during insmod:

  1. The finit_module(2) syscall (modern kernels) or init_module(2) receives the module's ELF bytes.
  2. The kernel calls load_module() in kernel/module/main.c.
  3. ELF sections are parsed and validated.
  4. The module's vermagic is checked against the running kernel.
  5. A memory region is allocated in module space using module_alloc().
  6. Relocations are applied: every U (undefined) symbol reference in your .ko is resolved against the kernel's exported symbol table (/proc/kallsyms).
  7. If CONFIG_MODVERSIONS is enabled, symbol CRCs are checked.
  8. Your __init function is called. If it returns 0, the module is live. If it returns non-zero, the kernel calls your exit function immediately and frees everything.

rmmod hello — calls delete_module(2). The kernel checks that no other module depends on yours and that your module's reference count is zero (no one is actively using it). Then it calls your __exit function and frees the module's memory region.

# Load the module (requires root / CAP_SYS_MODULE)
sudo insmod ./hello.ko

# Verify it is loaded
lsmod | grep hello
# hello                  16384  0
# (name, size in bytes, reference count)

# See it in /proc/modules (raw format)
grep hello /proc/modules

# See it in /sys/module/ (structured sysfs tree)
ls /sys/module/hello/
# coresize  holders  initsize  notes  parameters  refcnt  sections  taint

# Unload
sudo rmmod hello

# If rmmod fails with "Module is in use":
# Someone holds a reference. Find out who:
cat /sys/module/hello/holders/   # lists dependent modules

# Using modprobe for a module that's been depmod-registered:
sudo cp hello.ko /lib/modules/$(uname -r)/extra/
sudo depmod -a
sudo modprobe hello

06printk: The Kernel's Logging System

Inside the kernel, printf does not exist. There is no stdout, no FILE*, no libc. Instead you have printk() — the kernel's internal logging function, one of the most important functions in all of kernel code.

printk writes messages into the kernel ring buffer (/dev/kmsg, also readable via dmesg). The ring buffer is a circular, fixed-size buffer in kernel memory (default 512 KB, configurable via CONFIG_LOG_BUF_SHIFT). When it fills up, old messages are overwritten. This is why on a busy system with verbose logging you can miss messages.

Log levels. Every printk call should specify a log level as a prefix string. These are defined in <linux/kern_levels.h> and are simply 2-character string literals ("\001" + digit) that printk's format parser extracts:

MacroValueMeaning
KERN_EMERG"\0010"System is unusable
KERN_ALERT"\0011"Action must be taken immediately
KERN_CRIT"\0012"Critical conditions
KERN_ERR"\0013"Error conditions
KERN_WARNING"\0014"Warning conditions
KERN_NOTICE"\0015"Normal but significant
KERN_INFO"\0016"Informational
KERN_DEBUG"\0017"Debug-level messages

Messages at or above the console log level (/proc/sys/kernel/printk first field, default 7 = WARNING) are also printed to the system console in real time. Messages below that level go only to the ring buffer and must be read via dmesg.

pr_info, pr_err, dev_info, etc. Modern kernel code almost never calls printk directly. Instead it uses wrapper macros: pr_info(fmt, ...), pr_err(fmt, ...), pr_debug(fmt, ...). These expand to printk(KERN_INFO fmt, ...) etc., but also prepend the module name automatically via the pr_fmt macro. Driver code uses dev_info(dev, fmt, ...) which additionally prepends the device path — crucial for disambiguation in production.

pr_debug is special: it compiles to a no-op unless DEBUG is defined or CONFIG_DYNAMIC_DEBUG is enabled. Always use pr_debug for verbose tracing, never pr_info, so you do not flood production logs.

// Modern printk usage in a real module

// Define pr_fmt BEFORE including linux/module.h
// This prefix appears on every pr_*() message from this file
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

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

MODULE_LICENSE("GPL");

static int __init hello_init(void)
{
    pr_info("module loaded\n");          // KERN_INFO with module prefix
    pr_warn("this is a warning\n");      // KERN_WARNING
    pr_debug("only visible with DEBUG\n"); // no-op unless DEBUG defined

    // Raw printk — valid but old-style
    printk(KERN_INFO "raw printk message\n");

    // Print numeric values — same format specifiers as printf,
    // PLUS kernel-specific ones:
    printk(KERN_INFO "pointer: %p, kernel ptr safe: %pK\n",
           &hello_init, &hello_init);

    return 0;
}

static void __exit hello_exit(void)
{
    pr_info("module unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

// --- Reading the ring buffer ---
// dmesg | tail -5
// dmesg -w          (follow, like tail -f)
// dmesg --level=err,warn   (filter by level)
// cat /dev/kmsg     (raw, includes timestamps and sequence numbers)

07Module Parameters with module_param()

Hard-coded values in kernel modules are rarely useful. module_param() is the mechanism that lets you pass arguments to a module at load time — analogous to argv for userspace programs, but type-safe and also accessible via sysfs after loading.

The three-argument macro:

module_param(name, type, permissions);
  • name — the name of a static variable in your module (must be declared before the macro).
  • type — one of: bool, int, uint, long, ulong, charp (char pointer), short, ushort.
  • permissions — an octal bitmask for the sysfs file that will be created under /sys/module/<name>/parameters/. Use 0 to not create a sysfs entry, 0444 for read-only, 0644 for read-write (allows changing the parameter while the module is loaded!).

You must also call MODULE_PARM_DESC() — this is not optional for good kernel code. It populates the description shown by modinfo and is the only documentation for your parameter that users will see without reading your source.

Array parameters use module_param_array(name, type, nump, permissions) where nump is a pointer to an int that receives the number of elements actually provided by the user.

Security note: Be careful with 0644 permissions on parameters that affect kernel behavior in security-sensitive ways. An unprivileged read of /sys/module/your_module/parameters/secret_key is a real attack surface.

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("You");
MODULE_DESCRIPTION("Hello with parameters");

// Declare the variables FIRST
static char *whom = "world";   // default value
static int  howmany = 1;

// Register them as module parameters
module_param(whom,    charp, 0444);  // read-only from sysfs
module_param(howmany, int,   0644);  // read-write from sysfs

MODULE_PARM_DESC(whom,    "Who to greet (default: world)");
MODULE_PARM_DESC(howmany, "How many greetings (default: 1)");

static int __init hello_init(void)
{
    int i;
    if (howmany <= 0 || howmany > 100) {
        pr_err("howmany must be 1-100, got %d\n", howmany);
        return -EINVAL;  // standard errno: invalid argument
    }
    for (i = 0; i < howmany; i++)
        pr_info("Hello, %s! (greeting %d/%d)\n", whom, i + 1, howmany);
    return 0;
}

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

module_init(hello_init);
module_exit(hello_exit);

// --- Usage ---
// sudo insmod hello.ko whom="kernel" howmany=3
// modinfo hello.ko   # shows parm: lines
// cat /sys/module/hello/parameters/howmany  # read current value
// echo 5 | sudo tee /sys/module/hello/parameters/howmany  # change live

08The Build-Test-Debug Loop

Kernel development does not have a comfortable REPL. Your loop is: edit → build → load → observe → unload → repeat. Mastering this loop efficiently is one of the most valuable skills in kernel development.

The inner loop:

edit hello.c  →  make  →  sudo insmod hello.ko  →  dmesg | tail  →  sudo rmmod hello

Using a VM. For anything beyond hello-world, always develop inside a virtual machine. A kernel oops in a VM is a 10-second reboot. A kernel panic on your laptop means losing unsaved work across your desktop. QEMU with KVM is the standard choice. Tools like virtme-ng or syzkaller's VM harness make spinning up a test kernel trivial.

Reading kernel oops messages. When your module causes a kernel oops (a recoverable fault — the kernel logs the error but tries to continue), you will see a block like:

[  142.301924] BUG: kernel NULL pointer dereference, address: 0000000000000000
[  142.301926] RIP: 0010:hello_init+0x12/0x20 [hello]
[  142.301929] Call Trace:
[  142.301930]  do_one_initcall+0x41/0x1c0

The RIP line tells you: the fault was at hello_init + 0x12 bytes offset inside the hello module. Use objdump -d hello.ko to disassemble and find that offset.

addr2line and faddr2line. The kernel provides scripts/faddr2line to translate a function+offset into a source line number:

./scripts/faddr2line hello.ko hello_init+0x12

Dynamic debug. Once you move beyond hello-world, pr_debug messages can be enabled at runtime without recompiling:

echo 'module hello +p' | sudo tee /sys/kernel/debug/dynamic_debug/control

This is one of the most powerful kernel debugging tools available.

#!/bin/bash
# A simple development helper script — save as dev.sh
# Usage: ./dev.sh [build|load|unload|reload|log]

MOD=hello
KO=./${MOD}.ko

case "$1" in
  build)
    make && modinfo $KO | grep -E 'vermagic|license|description'
    ;;
  load)
    sudo insmod $KO "${@:2}" && echo "Loaded" && dmesg | tail -5
    ;;
  unload)
    sudo rmmod $MOD && echo "Unloaded" && dmesg | tail -3
    ;;
  reload)
    sudo rmmod $MOD 2>/dev/null; sudo insmod $KO "${@:2}" && dmesg | tail -5
    ;;
  log)
    # Follow the kernel log live — Ctrl-C to stop
    dmesg -w
    ;;
  *)
    echo "Usage: $0 {build|load|unload|reload|log}"
    ;;
esac

# One-liner debug build with pr_debug enabled:
# make ccflags-y=-DDEBUG && sudo insmod hello.ko && dmesg | tail

09Module Licensing, Taint, and the GPL Boundary

The MODULE_LICENSE() macro is not just metadata — it has real mechanical consequences inside the kernel.

When the kernel loads a module, it inspects the license string. If the license is not a recognized open-source license (GPL, GPL v2, GPL and additional rights, Dual MIT/GPL, Dual BSD/GPL, etc.), the kernel sets a taint flag in tainted (readable at /proc/sys/kernel/tainted). A tainted kernel means:

  1. Many kernel developers will ignore your bug reports.
  2. WARN_ON macros may suppress output.
  3. Some kernel subsystems refuse to operate in tainted mode.
  4. Crash dumps submitted to distributions are deprioritized.

More importantly: the kernel exports two classes of symbols — EXPORT_SYMBOL and EXPORT_SYMBOL_GPL. GPL-only symbols (the majority of interesting ones: usb_register, request_irq, alloc_netdev, etc.) are only accessible to modules with a GPL-compatible license. If your module declares MODULE_LICENSE("Proprietary") and tries to use a GPL-only symbol, insmod will fail with ERROR: insmod: FATAL: Module hello not permitted to use GPL-only symbol 'xyz'.

This is a deliberate policy decision by Linus Torvalds and the kernel maintainers. The kernel's exported ABI is considered part of a GPL-licensed work, and they use EXPORT_SYMBOL_GPL to enforce that commercial vendors who want to ship proprietary kernel modules cannot simply call into the kernel's core infrastructure without a GPL-compatible license.

For learning and contributing: always use MODULE_LICENSE("GPL") or MODULE_LICENSE("GPL v2"). It is the right choice technically and philosophically.

// Illustration of what tainting looks like
// DO NOT use "Proprietary" in real code

MODULE_LICENSE("Proprietary");  // sets TAINT_PROPRIETARY_MODULE bit

// After insmod:
// $ cat /proc/sys/kernel/tainted
// 4096   (bit 12 = TAINT_UNSIGNED_MODULE or similar)
//
// $ dmesg | grep taint
// hello: module license 'Proprietary' taints kernel.
// Disabling lock debugging due to kernel taint

// --- Checking exported symbol class ---
// GPL-only symbols have '\t[gpl]' in /proc/kallsyms:
// grep request_irq /proc/kallsyms
// ffffffff813abc00 T request_irq   <- GPL-only in some kernel versions

// The correct declaration for a learning/contribution module:
MODULE_LICENSE("GPL v2");

Key insights

  • ✦A .ko file is a relocatable ELF object (ET_REL), not an executable — the kernel's module loader is essentially a mini dynamic linker running at ring 0.
  • ✦Your Makefile does not compile your module directly; it delegates to Kbuild inside the kernel tree, which supplies all the compiler flags, ABI checks, and version magic.
  • ✦The vermagic string is a safety lock: a module built against the wrong kernel headers will be refused at load time, protecting you from silent ABI mismatches that could corrupt memory silently.
  • ✦printk writes to an in-memory ring buffer, not stdout. If you do not read dmesg, your module's output simply disappears. Always tail dmesg after insmod.
  • ✦MODULE_LICENSE is not optional boilerplate — it gates access to EXPORT_SYMBOL_GPL symbols, which includes the vast majority of interesting kernel subsystem APIs.
  • ✦The __init and __exit annotations are a memory optimization: for built-in code, the kernel literally frees the init section after boot. Always annotate your init/exit functions.
  • ✦Returning a negative errno from your init function is the correct way to signal failure — the kernel will call your exit function immediately and refuse to load the module.
  • ✦Developing in a VM is not optional caution — it is the standard professional workflow. A kernel panic in QEMU is a 10-second inconvenience; on bare metal it disrupts everything else you have open.

Common pitfalls

  • ⚠Forgetting MODULE_LICENSE: the module loads but taints the kernel and locks you out of GPL-only exported symbols. Always declare it, always use a GPL-compatible value.
  • ⚠Building against the wrong kernel headers: if /lib/modules/$(uname -r)/build points to a different kernel version than what is running, vermagic will not match and insmod fails with 'Invalid module format'. Rebuild after every kernel update.
  • ⚠Using printf, malloc, or any libc function: there is no libc in kernel space. These symbols do not exist. Use printk/pr_info and kmalloc/kfree instead.
  • ⚠Forgetting the newline at the end of printk format strings: printk does not add one automatically. Without \n, messages from different printk calls can concatenate into unreadable noise in dmesg.
  • ⚠Running `make` as root: always build as a normal user and only use sudo for insmod/rmmod. Building as root can create files owned by root in your source tree, breaking future non-root builds.
  • ⚠Ignoring the return value of the init function: if your init function always returns 0 even when a resource allocation fails, you will leak resources and potentially hang the system silently.
  • ⚠Testing on a bare-metal machine instead of a VM: a kernel oops or panic on your laptop means losing unsaved work across your entire desktop environment. Always use QEMU or another VM for module development.
  • ⚠Using pr_info for debug tracing: use pr_debug instead, which compiles away to nothing unless DEBUG is defined. Shipping a module with verbose pr_info calls will flood production system logs.

Practice

  1. 01
    easy

    Build and load the hello-world module exactly as shown. Then deliberately break it: return -ENOMEM from hello_init instead of 0. Rebuild, try to insmod, and explain what you see in dmesg. Then fix it and reload successfully.

  2. 02
    easy

    Add a module_param of type 'int' named 'delay_ms' with a default of 0 and sysfs permissions 0644. In hello_init, if delay_ms > 0, use msleep(delay_ms) to sleep that many milliseconds before printing. Load it with delay_ms=500 and measure whether the load time increases. Then change the value live via sysfs and reload to confirm the new value is picked up.

  3. 03
    medium

    Write a module with TWO source files: hello_core.c (containing init/exit) and hello_util.c (containing a function 'hello_greet(const char *name)' that does the printk). hello_core.c calls hello_greet(). Update the Makefile to link both into a single hello.ko. Verify with nm hello.ko that hello_greet appears as a defined symbol.

  4. 04
    medium

    Intentionally cause a NULL pointer dereference in hello_init by doing: int *p = NULL; *p = 42; Load the module in a VM and capture the full oops message from dmesg. Use 'objdump -d hello.ko' to find the instruction at the offset reported in the RIP line. Explain exactly what assembly instruction caused the fault.

  5. 05
    hard

    Write a module that counts how many times it has been loaded (since the last boot) by storing the count in a sysfs-exposed parameter. Implement a proper reset mechanism: if the module is loaded with reset=1 as a parameter, the counter resets to 0. Use a static variable in the .bss section (zero-initialized) to persist the count across rmmod/insmod cycles. Verify that the counter actually persists across remove/reload cycles.

Resources

Checkpoints — you can now…

  • ✓You can write a minimal kernel module from scratch (no template), build it with a correct two-phase Kbuild Makefile, and explain what each line of the Makefile does.
  • ✓You can load your module with insmod, verify it is live with lsmod, read its output with dmesg, and cleanly remove it with rmmod — all without needing to look anything up.
  • ✓You can use modinfo to inspect a .ko file's vermagic, license, and parameter descriptions before loading it, and explain why vermagic mismatches prevent loading.
  • ✓You can add module_param() declarations, load the module with custom parameter values on the command line, and read or update parameters live via /sys/module/<name>/parameters/.
  • ✓You can explain the difference between insmod and modprobe, between EXPORT_SYMBOL and EXPORT_SYMBOL_GPL, and why MODULE_LICENSE("GPL") is the correct choice for kernel contribution work.
  • ✓You can read a kernel oops message in dmesg, identify the faulting instruction offset, and use objdump to locate the corresponding line in your module's disassembly.
  • ✓You can articulate why developing kernel modules in a VM rather than on bare metal is the professional standard, and you have set up or can set up a QEMU-based kernel dev environment.