The Code
Line by Line
The Makefile
Build & Run
printk & pr_info
Kernel Headers
Interview Q&A
Keywords:
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.
- 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
- Includes:
linux/module.h,linux/kernel.h - Uses
pr_info()orprintk()to log - Uses
kmalloc()for memory - Has
module_init()andmodule_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.
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/. 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.
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;
}
| 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
make will give you a cryptic “missing separator” error.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.build symlink points to /usr/src/linux-headers-$(uname -r). This is where Kbuild lives.-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:
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.
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
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.
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.
Verify it’s loaded
$ lsmod | grep hello_lkm
hello_lkm 16384 0
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.
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.
| 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.
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.
#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 */
#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.
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.
#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.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.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.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.-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.__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.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().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.