Writing Your First Kernel Module — Hello World!-Free Linux kernel development course

Previous Lecture
Next Lecture

Linux Kernel Programming
Chapter 4  |  Part 3 of 6
Writing Your First Kernel Module — Hello World!
📚 Hands-On
⏱ ~25 min
🔐 Kernel 6.x

🔍 Topics Covered
Complete Module Source Code
Required Headers
module_init / module_exit
Building with make
insmod / rmmod
dmesg output
Common Build Errors
Setup Requirements

🔧 Prerequisites: What You Need on Your System

Before you can write and load a kernel module, your development machine needs two things installed and working together:

1. A Toolchain — This is the GCC compiler, assembler, linker, and make utility. On Ubuntu/Debian:

sudo apt update
sudo apt install build-essential

build-essential pulls in gcc, g++, make, and other needed tools in one shot.

2. Kernel Headers — Headers for the exact kernel version running on your machine. Your module will be compiled against these headers.

# Install headers matching your running kernel
sudo apt install linux-headers-$(uname -r)

# Verify installation:
ls /lib/modules/$(uname -r)/build
# You should see a directory — not "No such file or directory"

On Fedora/RHEL/CentOS systems, the equivalent commands are:

sudo dnf install gcc make kernel-devel kernel-headers

On Arch Linux:

sudo pacman -S base-devel linux-headers

Important note for kernel 6.x: Starting from kernel 6.3, several header files were reorganized. If you are on kernel 6.3 or newer and your module uses older header paths, you may get include errors. The module we write below uses paths that work from kernel 5.4 all the way through 6.x.

📄 The Complete Hello World Kernel Module

Create a new directory for your module and create the source file:

mkdir ~/hello_module
cd ~/hello_module

Now create a file called hello.c with the following content:

/*
 * hello.c - A simple Hello World Linux Kernel Module
 *
 * Demonstrates the basic structure of every kernel module:
 *   - Required includes
 *   - Module metadata
 *   - Init function (called on insmod)
 *   - Exit function (called on rmmod)
 *
 * Tested on Linux kernel 5.15 through 6.x
 */

#include <linux/module.h>   /* Required for all kernel modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO, pr_info() */
#include <linux/init.h>     /* Needed for __init and __exit macros */

/* ---- Module Metadata ---- */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your@email.com>");
MODULE_DESCRIPTION("Hello World - First Kernel Module");
MODULE_VERSION("1.0");

/*
 * hello_init - called when module is loaded with insmod
 *
 * Returns 0 on success, negative errno on failure.
 * Returning non-zero aborts the load.
 */
static int __init hello_init(void)
{
    pr_info("Hello World! Module loaded successfully.\n");
    pr_info("I am now running inside the kernel!\n");
    return 0;  /* 0 = success */
}

/*
 * hello_exit - called when module is removed with rmmod
 *
 * Must undo everything hello_init() did.
 * No return value.
 */
static void __exit hello_exit(void)
{
    pr_info("Goodbye! Module unloaded cleanly.\n");
}

/* Register our init and exit functions with the LKM framework */
module_init(hello_init);
module_exit(hello_exit);

Let us walk through every part of this code carefully.

🔎 Dissecting the Hello World Module — Line by Line

The Three Essential Headers
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

linux/module.h — The most important header for any kernel module. It defines module_init(), module_exit(), MODULE_LICENSE(), and the core structures the kernel uses to manage modules. You cannot write a module without this.

linux/kernel.h — Provides pr_info(), pr_err(), KERN_INFO, and many common macros and type definitions used throughout kernel code.

linux/init.h — Defines the __init and __exit section markers (explained below).

The __init and __exit Markers
static int __init hello_init(void)   { ... }
static void __exit hello_exit(void)  { ... }

__init tells the compiler to place the init function in a special memory section called .init.text. After the module’s init function has run and the module is loaded, the kernel can free the memory occupied by that section because the init function will never be called again. This saves RAM — important for embedded systems.

__exit places the exit function in a .exit.text section. If the module is compiled as built-in (Y, not M), the exit function is never needed (you cannot unload built-in code), so the kernel can simply discard it at compile time.

static means these functions are private to this translation unit. They are not exported and cannot be called directly by other kernel code — the kernel only calls them through the registered function pointers.

pr_info() — The printk Wrapper
pr_info("Hello World! Module loaded successfully.\n");

pr_info() is a convenience wrapper around printk() that automatically prepends the KERN_INFO log level. It is the preferred way to print informational messages in modern kernel code.

The output does not go to your terminal — it goes to the kernel ring buffer, which you read with dmesg. Part 5 of this series covers printk and log levels in full detail.

Return Value of the Init Function
return 0;  /* 0 = success, negative = failure */

The init function must return 0 to signal success. Any negative value signals failure and the kernel will not load the module. Standard error codes from <linux/errno.h> are used: -ENOMEM for memory allocation failure, -ENODEV for device not found, etc. Returning positive values is undefined behaviour in older kernels and should be avoided.

🔧 The Makefile — Simple Version

In the same directory as hello.c, create a file named exactly Makefile (capital M):

# Makefile for hello.c kernel module

# 'obj-m' tells the kernel build system to build hello.c as a module
obj-m += hello.o

# Path to the running kernel's build directory
KDIR := /lib/modules/$(shell uname -r)/build

# Default target: build the module
all:
	make -C $(KDIR) M=$(PWD) modules

# Clean target: remove all build artifacts
clean:
	make -C $(KDIR) M=$(PWD) clean

Critical: The lines under all: and clean: must start with a TAB character, not spaces. If you use spaces, make will give you a “missing separator” error.

Let us understand what this Makefile does:

obj-m += hello.o — Registers hello.o as an out-of-tree module to be built. The kernel build system knows it should compile hello.c and link it into hello.ko.
KDIR := /lib/modules/$(shell uname -r)/build — Points to the kernel headers/build directory for your currently running kernel. The symlink here points to the actual kernel source or header tree.
make -C $(KDIR) M=$(PWD) modules — Changes into the kernel build directory (-C), tells it this module is located at your current directory (M=$(PWD)), and requests a module build.

⚙ Build, Load, Test, and Unload

Now let us go through the complete workflow step by step:

Step 1 — Build the module
cd ~/hello_module
make

# Expected output (abbreviated):
# make -C /lib/modules/6.8.0-45-generic/build M=/home/user/hello_module modules
# make[1]: Entering directory '/usr/src/linux-headers-6.8.0-45-generic'
#   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
# make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-45-generic'

After a successful build, your directory will contain hello.ko — your kernel module file, ready to load.

Step 2 — Load the module (requires root)
sudo insmod hello.ko

# insmod gives no output on success.
# If it fails, check dmesg for the reason.

insmod (insert module) is the low-level command to load a .ko file. It does not resolve dependencies — it just loads the exact file you give it.

Step 3 — Verify it is loaded and see the output
# Check if the module is listed in the running kernel:
lsmod | grep hello
# Output:
# hello                  16384  0

# See the printk output from our init function:
dmesg | tail -5
# Output:
# [12345.678901] Hello World! Module loaded successfully.
# [12345.678910] I am now running inside the kernel!

Step 4 — Unload the module
sudo rmmod hello

# Check it is gone:
lsmod | grep hello
# (no output — module removed)

# See the exit function's message:
dmesg | tail -3
# Output:
# [12390.123456] Goodbye! Module unloaded cleanly.

Step 5 — Clean the build directory
make clean
# Removes: hello.o, hello.ko, hello.mod.c, hello.mod.o,
#          Module.symvers, modules.order, .tmp_versions/

💡 What Actually Happens Inside the Kernel When You Run insmod?
insmod Internals — Step by Step
1
insmod reads the .ko file into memory in user space and calls the finit_module() system call (or the older init_module()).
2
The kernel verifies the module: checks vermagic (version + config match), validates ELF structure, and checks the module signature if module signing is enforced.
3
The kernel allocates memory in a special region (module memory area, distinct from the main kernel heap) and copies the module’s code and data into it.
4
Symbol resolution: The kernel resolves all external symbols the module uses (like printk) by looking them up in the kernel’s exported symbol table (kallsyms). Unresolved symbols cause the load to fail.
5
The module is added to the kernel’s internal linked list of loaded modules (visible via /proc/modules and lsmod).
6
The init function is called. If it returns 0, loading is complete. If it returns an error, the module is removed and memory freed.

🚧 Common Errors and How to Fix Them
Error: “make: *** No rule to make target ‘modules’. Stop.”
Cause: The Makefile’s indentation uses spaces instead of a real TAB character.
Fix: Open the Makefile in a proper text editor, delete the whitespace before make -C ... and press the Tab key once.
Error: “insmod: ERROR: could not insert module hello.ko: Invalid module format”
Cause: The module was compiled for a different kernel version than what is running (vermagic mismatch).
Fix: Run uname -r, ensure linux-headers-$(uname -r) is installed, then rebuild with make clean && make.
Error: “insmod: ERROR: could not insert module hello.ko: Operation not permitted”
Cause: You are not running as root, or Secure Boot is enabled and the module is not signed.
Fix: Use sudo insmod hello.ko. For Secure Boot, either disable it or sign the module.
Error: “fatal error: linux/module.h: No such file or directory”
Cause: Kernel headers are not installed.
Fix: sudo apt install linux-headers-$(uname -r)
Error: “rmmod: ERROR: Module hello is in use”
Cause: Something (another module or a process) is still using your module.
Fix: Check lsmod — the third column shows the use count. Close any programs using the module, or remove dependent modules first.

🏆 Interview Questions — Writing Kernel Modules
Q1. What is the purpose of the __init macro in a kernel module?
__init marks the function to be placed in the .init.text section. After the module loads and the init function runs once, the kernel can free the memory used by that section since the init function will never be called again. This saves RAM, which matters especially in embedded systems with limited memory.
Q2. Which three headers are always needed for a kernel module?
linux/module.h (core module framework), linux/kernel.h (pr_info, log levels, common macros), and linux/init.h (__init and __exit macros). In practice, many modules need more headers depending on what subsystems they use.
Q3. What does obj-m mean in the kernel module Makefile?
obj-m (object-module) tells the kernel build system (Kbuild) that the listed object file should be compiled as a loadable module (.ko). The alternative is obj-y, which compiles it as built-in kernel code.
Q4. What is the difference between insmod and modprobe?
insmod loads the exact .ko file you specify with no dependency resolution. modprobe is a higher-level tool that reads /lib/modules/$(uname -r)/modules.dep, automatically resolves and loads all dependencies, and can load a module by name without knowing its path. For production use, modprobe is preferred.
Q5. Where does printk / pr_info output go and how do you see it?
It goes to the kernel ring buffer. You can read it with the dmesg command. On systemd-based systems, you can also use journalctl -k to see kernel messages. The ring buffer is a circular buffer, so old messages are overwritten as new ones arrive when it fills up.

Up Next: Part 4 — Common Module Operations
Learn insmod, rmmod, lsmod, modinfo, modprobe in depth with real examples.

← Part 2
Part 4: Module Operations →

Previous Lecture
Next Lecture

Leave a Reply

Your email address will not be published. Required fields are marked *