How to Write Your First Kernel Module

 

Prev_Lec
Next_Lec
How to Write Your First Kernel Module – Free Linux Kernel Development Course
How to Write Your First Kernel Module – Hello World LKM on Linux Kernel 6.x, Explained Line by Line
⏱ 25 min read
🐧 Kernel 6.x
🆓 100% Free
💻 Hands-On

Keywords:

write kernel module
module_init
module_exit
__init __exit
MODULE_LICENSE
printk pr_info
Makefile kbuild
insmod rmmod
dmesg
linux/module.h
kernel headers
KERN_INFO

Before You Begin

Writing a kernel module is very different from writing a normal C program. Before you write even one line, make sure you understand what you’re getting into.

User-Space C vs Kernel-Space C – Key Differences
👤 Normal C Program (User Space)
  • Includes: stdio.h, stdlib.h
  • Uses printf() to print
  • Uses malloc() for memory
  • Has a main() function
  • If it crashes, only your process dies
  • Compiled with: gcc myfile.c -o myapp
⚙️ Kernel Module (Kernel Space)
  • Includes: linux/module.h, linux/kernel.h
  • Uses pr_info() or printk() to log
  • Uses kmalloc() for memory
  • Has module_init() and module_exit()
  • If it crashes, the whole system can crash
  • Compiled with: kbuild system (make -C /lib/modules/...)

On your development machine (Ubuntu 22.04 or 24.04 is ideal), you need two things installed before you can compile any kernel module:

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

The linux-headers-$(uname -r) package gives you the header files and Kbuild infrastructure for your running kernel. Without them, you simply cannot compile any module.

Kernel 6.x tip: On Ubuntu 24.04 (which ships kernel 6.8.x), these packages are available directly from the main repository. Run uname -r first to confirm your exact kernel version, then install headers for that specific version.

The Hello World Kernel Module

Here is a minimal, working Hello World kernel module. This is your own code — not copied from any book. Write it fresh in a file called hello_lkm.c:

/*
 * hello_lkm.c
 * A minimal Hello World Linux Kernel Module.
 * Works on Linux Kernel 5.x and 6.x.
 *
 * Author: Your Name
 * License: GPL v2
 */

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

MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("EmbeddedPathashala: Hello World Kernel Module");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.0");

static int __init hello_lkm_init(void)
{
    pr_info("hello_lkm: module loaded – Hello from kernel space!\n");
    return 0;
}

static void __exit hello_lkm_exit(void)
{
    pr_info("hello_lkm: module unloaded – Goodbye from kernel space!\n");
}

module_init(hello_lkm_init);
module_exit(hello_lkm_exit);

That’s the complete code. Small, but every single line has a specific purpose. Let’s go through each one.

Line by Line – Understanding Every Part

The Header Files

#include <linux/module.h>
The most important header for any kernel module. Defines module_init(), module_exit(), MODULE_LICENSE(), EXPORT_SYMBOL(), and everything related to the LKM framework. You cannot write a module without this.
#include <linux/kernel.h>
Provides pr_info(), pr_err(), and other pr_* logging macros. Also defines KERN_INFO, KERN_ERR and other log level constants used by printk(). Without this, your logging calls won’t compile.
#include <linux/init.h>
Defines the __init and __exit macros. These are optimization hints that tell the kernel linker to place the init function in a special memory section (.init.text) that can be freed after the module is initialized. Optional but considered good practice.
Where do these headers come from? These are kernel headers, not the standard C library (glibc) headers. They live inside the kernel source tree under include/linux/. When you install linux-headers-$(uname -r), these headers get installed to /usr/src/linux-headers-$(uname -r)/include/linux/. The kbuild system automatically tells the compiler to look there.

The Module Metadata Macros

The four MODULE_* macros don’t generate executable code. They embed metadata strings directly into the .ko file’s special section called .modinfo. This is exactly what modinfo reads when you inspect a module.

MODULE_AUTHOR(“…”)
Your name or email. Appears in modinfo output under the author: field. No functional effect on the module.
MODULE_DESCRIPTION(“…”)
A short human-readable description. Helps system administrators understand what a module does when they run modinfo.
MODULE_LICENSE(“GPL”)
The most important macro. This tells the kernel what license your module uses. If you use “GPL”, you get access to GPL-only exported kernel symbols. If you use “Proprietary”, the kernel marks itself as tainted when your module loads, and you lose access to many internal kernel functions. Always use “GPL” for educational and open-source work.
MODULE_VERSION(“1.0”)
Your module’s version string. Optional, but good practice for tracking module revisions.

The Init Function – What Runs on insmod

static int __init hello_lkm_init(void)
{
    pr_info("hello_lkm: module loaded – Hello from kernel space!\n");
    return 0;
}

Anatomy of the Init Function
Part What it does
static Prevents the function name from being exported globally. Good practice to avoid name collisions in the global kernel symbol table.
int The init function must return an int. Return 0 for success. Any non-zero value tells the kernel the init failed, and the module will not be loaded.
__init A macro that places this function in the .init.text ELF section. After the module finishes initializing, the kernel can reclaim this memory. This is purely an optimization hint — the code works without it.
hello_lkm_init You can name this anything you want. It becomes the init function only because of module_init(hello_lkm_init) below. The name itself is not magic.
return 0 Zero means success. The kernel will continue loading the module. Return a negative error code like -ENOMEM or -ENODEV if something goes wrong during setup.

The Exit Function – What Runs on rmmod

static void __exit hello_lkm_exit(void)
{
    pr_info("hello_lkm: module unloaded – Goodbye from kernel space!\n");
}

The exit function must be void — it cannot fail. By the time the kernel calls this function, it has already decided to unload the module. There is no way to abort the unloading from inside module_exit(). This is why your cleanup code must be reliable.

The __exit macro is the counterpart to __init. It places the function in a section that is discarded if the module is compiled directly into the kernel (built-in modules can never be unloaded, so the exit code would be wasted memory).

The Registration Macros

module_init(hello_lkm_init);
module_exit(hello_lkm_exit);

These two macros are how you tell the LKM framework which function to call when loading and unloading. Internally, they store pointers to your functions inside the kernel module’s struct module. When insmod loads your .ko, the kernel finds these pointers and calls the init function through them.

The Makefile – Compiling with Kbuild

You cannot compile a kernel module with a plain gcc command. The kernel has its own build system called Kbuild. You must go through Kbuild so your module gets the correct compiler flags, header search paths, and linker settings for the running kernel. Here is the Makefile you need:

# Makefile for hello_lkm
# Place this in the same directory as hello_lkm.c

obj-m += hello_lkm.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
Critical: The indentation inside the Makefile must be a TAB character, not spaces. This is a make requirement. If you use spaces, make will give you a cryptic “missing separator” error.

What Each Line of the Makefile Does
obj-m += hello_lkm.o
Tells Kbuild to build hello_lkm.o as a module (the -m suffix). If you wrote obj-y, it would build into the kernel image instead. The .o becomes a .ko after linking.
KDIR := /lib/modules/$(shell uname -r)/build
Points to the directory containing the kernel build infrastructure for the currently running kernel. The build symlink points to /usr/src/linux-headers-$(uname -r). This is where Kbuild lives.
$(MAKE) -C $(KDIR) M=$(PWD) modules
-C $(KDIR) changes into the kernel source directory first (so Kbuild takes over). M=$(PWD) tells Kbuild to come back and build the module files in your current directory. The modules target triggers module compilation.

Build, Load, and Test Your Module

With your source file and Makefile both in the same directory, follow these steps:

1

Compile the module

$ make
make -C /lib/modules/6.8.0-xx-generic/build M=/home/ravi/hello_lkm modules
  CC [M]  /home/ravi/hello_lkm/hello_lkm.o
  MODPOST /home/ravi/hello_lkm/Module.symvers
  CC [M]  /home/ravi/hello_lkm/hello_lkm.mod.o
  LD [M]  /home/ravi/hello_lkm/hello_lkm.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-xx-generic'

You should see hello_lkm.ko created in your directory. That’s your kernel module binary.

2

Check the module metadata

$ modinfo hello_lkm.ko
filename:       /home/ravi/hello_lkm/hello_lkm.ko
version:        1.0
description:    EmbeddedPathashala: Hello World Kernel Module
author:         Your Name
license:        GPL
srcversion:     ...
vermagic:       6.8.0-xx-generic SMP preempt mod_unload
3

Load the module

$ sudo insmod hello_lkm.ko

If nothing appears on the terminal, that’s expected. Kernel logs go to the kernel ring buffer, not to stdout.

4

Check the kernel log

$ dmesg | tail -5
[12345.678901] hello_lkm: loading out-of-tree module taints kernel.
[12345.678950] hello_lkm: module loaded – Hello from kernel space!

You will see a “taints kernel” warning for any out-of-tree module. This is normal and expected during development. The line below it is your pr_info() message.

5

Verify it’s loaded

$ lsmod | grep hello_lkm
hello_lkm              16384  0
6

Unload the module

$ sudo rmmod hello_lkm
$ dmesg | tail -3
[12367.123456] hello_lkm: module unloaded – Goodbye from kernel space!

Your exit function ran. The module is now gone from kernel memory. You can verify with lsmod | grep hello_lkm — it should show nothing.

Files Created by the Build Process
hello_lkm.ko
The actual kernel module you load with insmod. This is your deliverable.
hello_lkm.o
Your source compiled to an object file. Intermediate step before linking into .ko.
hello_lkm.mod.c
Auto-generated by Kbuild. Contains the module’s version info and symbol table entries.
Module.symvers
Lists exported symbols and their CRC checksums. Important when your module exports symbols for others to use.

Kernel Logging – printk, pr_info, and Log Levels

In user space you use printf(). In the kernel there is no standard C library, so you use printk() (kernel printf). However, since Linux 3.x, the recommended approach is to use the pr_* family of macros, which are wrappers around printk() that automatically prepend the calling module’s name and the log level.

Kernel Log Levels – From Most to Least Severe
Level String pr_* Macro Numeric Level When to Use
KERN_EMERG pr_emerg() 0 System is unusable – imminent crash
KERN_ALERT pr_alert() 1 Action must be taken immediately
KERN_CRIT pr_crit() 2 Critical hardware or software failure
KERN_ERR pr_err() 3 Error conditions – use for driver errors
KERN_WARNING pr_warn() 4 Warning – something unusual but not fatal
KERN_NOTICE pr_notice() 5 Normal but significant events
KERN_INFO pr_info() 6 Informational – use this most of the time during learning
KERN_DEBUG pr_debug() 7 Debug messages – only visible if dynamic debug is enabled

For your learning modules, always use pr_info(). For error handling paths, use pr_err(). The printk(KERN_INFO "...") style still works in kernel 6.x, but the pr_info() form is cleaner and preferred in modern kernel code.

Viewing logs on kernel 6.x: Use dmesg -T to see human-readable timestamps. On systems with journald (most Ubuntu 22.04+ and Fedora systems), you can also run sudo journalctl -k -f to watch kernel messages in real time as you load and unload modules.

A Note on Kernel Headers vs glibc Headers

This trips up almost every beginner. You must never mix kernel headers with glibc (standard C library) headers in a kernel module.

Kernel Headers vs Standard C Library Headers
❌ NEVER do this in a kernel module
#include <stdio.h>      /* glibc - WRONG */
#include <stdlib.h>     /* glibc - WRONG */
#include <string.h>     /* glibc - WRONG */

printf("hello\n");      /* WRONG – no stdio in kernel */
malloc(100);            /* WRONG – no heap allocator */
✅ Always use kernel equivalents
#include <linux/kernel.h>   /* kernel logging */
#include <linux/slab.h>     /* kernel memory */
#include <linux/string.h>   /* kernel strings */

pr_info("hello\n");     /* kernel logging */
kmalloc(100, GFP_KERNEL); /* kernel memory */

The reason is simple: glibc assumes it’s running in user space with virtual memory, system calls, and a C runtime. The kernel doesn’t have any of that when a module is executing. Mixing them leads to compile errors at best, and bizarre runtime crashes at worst.

Bonus: What Does __init Actually Do Under the Hood?

This is something most tutorials skip over, but it’s worth knowing for interviews.

The __init macro expands to __attribute__((__section__(".init.text"))). This is a GCC compiler directive that places the marked function into a special ELF section of the .ko file called .init.text instead of the regular .text section.

How __init Saves Kernel Memory
hello_lkm.ko (ELF)
.text
hello_lkm_exit() lives here
.data / .rodata
module strings, constants
.init.text
hello_lkm_init() is here
.modinfo
author, license, vermagic
After insmod + init runs
.text stays in memory
exit function still needed
.data stays in memory
.init.text FREED
init function no longer needed
.modinfo stays

For a tiny Hello World module, this memory saving is negligible. But for large drivers with complex initialization routines, the __init annotation can save several kilobytes of kernel memory per module — and at scale across hundreds of loaded modules, this adds up.

🎯 Interview Questions – Writing Kernel Modules

Practice these answers before your embedded Linux or kernel driver interviews.

Q1. What are the minimum required components of a valid Linux kernel module?
Three things are absolutely mandatory. First, #include <linux/module.h>. Second, MODULE_LICENSE() — without this the kernel refuses to load the module in modern kernels. Third, at least one of module_init() or module_exit() — technically a module with no init function is allowed (it becomes a no-op), but having neither serves no purpose. In practice you always write both.
Q2. What is the return value of a module’s init function, and what does a non-zero return do?
The init function returns an int. Returning 0 signals success to the kernel, and module loading proceeds. Returning any non-zero value (conventionally a negative error code like -ENOMEM, -ENODEV) signals failure. The kernel aborts the load, calls no cleanup, and reports the error to the insmod caller. The errno values from <linux/errno.h> should be used as error returns.
Q3. Why is the exit function void? Can you prevent a module from being unloaded?
The exit function is void because by the time it is called, the decision to unload is final. You cannot abort it from inside the exit function. You can prevent a module from being unloaded by keeping its reference count greater than zero — for example, by having an open file descriptor to a device the module manages. The kernel will refuse rmmod with “device busy” until the reference count drops to zero.
Q4. What is the difference between printk() and pr_info()?
printk(KERN_INFO "message\n") is the original low-level kernel logging function. pr_info("message\n") is a macro wrapper (defined in linux/kernel.h) that automatically sets the log level to KERN_INFO and prepends the pr_fmt() format string — which typically includes the module name. In practice they produce identical output, but pr_info() and friends are preferred in kernel 4.x and above because they produce cleaner, more readable code.
Q5. What does the kbuild command “make -C $(KDIR) M=$(PWD) modules” actually do?
It first changes directory (-C) into the kernel build directory ($(KDIR)) where the Kbuild Makefiles live. Then it passes M=$(PWD) to tell Kbuild that external module source files are in the current directory (PWD). Finally, the modules target triggers the out-of-tree module build process, which compiles your .c files with the correct kernel compiler flags and links them into a .ko file.
Q6. What happens if you accidentally include a glibc header like stdio.h in a kernel module?
At best, you will get compiler errors because glibc headers use features that aren’t available in the kernel environment (like __FILE__ macro conflicts, missing size_t definitions, or redefined types). At worst, if it somehow compiles, it will link functions that don’t exist in kernel space, leading to undefined symbol errors at module load time or catastrophic runtime behavior.
Q7. What is MODULE_LICENSE() and why can’t you omit it?
Starting from around kernel 5.12+, omitting MODULE_LICENSE() causes a hard build error — the module simply won’t compile. Before that, omitting it was treated as “Proprietary” and caused a kernel taint. The license macro does two things: it embeds the license string in .modinfo for modinfo to display, and it determines whether the module can access GPL-only exported kernel symbols. Without MODULE_LICENSE("GPL"), you cannot call many internal kernel functions that are exported with EXPORT_SYMBOL_GPL().
Q8. How do you read kernel log output from a loaded module?
The primary tool is dmesg. Running dmesg | tail -20 shows the last 20 lines of the kernel ring buffer. Use dmesg -T for human-readable timestamps, or dmesg -w (or dmesg --follow) to watch messages in real time. On systemd-based distributions (Ubuntu 22.04+, Fedora), you can also use sudo journalctl -k -f to follow kernel messages. On embedded systems without journald, dmesg is always available.

You Just Wrote Your First Kernel Module! 🎉

In the next part, we go deeper — module parameters, exporting symbols, and understanding the kernel symbol table.

Prev_lec
Next_Lec

Leave a Reply

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