Linux kernel modules are pieces of code that can be loaded into or removed from the Linux kernel without rebuilding the entire kernel. They are widely used for device drivers, hardware support, networking, and other kernel-level functionality. In this tutorial, you will learn how to write a simple kernel module in C, compile it using a Makefile, load it into the Linux kernel, verify its output, and safely unload it.Linux is widely used in embedded systems, automotive electronics, networking devices, industrial controllers, and Internet of Things (IoT) applications. One of the reasons for its popularity is its ability to support different hardware devices and system features through a flexible kernel architecture. At the heart of Linux is the Linux kernel, which manages hardware resources, memory, processes, and communication between software and hardware. However, the kernel does not need to contain every possible driver or feature permanently. Many components can be added when required through loadable kernel modules. For example, imagine developing an embedded Linux device that uses a particular sensor, communication interface, or hardware controller. Instead of rebuilding the entire kernel whenever you need to add compatible functionality, you may be able to develop and load a suitable kernel module. Learning Linux kernel module programming is therefore an important step for students interested in embedded Linux, device driver development, operating systems, and low-level C programming. In this Linux kernel module tutorial, we will build a simple module that prints a message when loaded and another message when unloaded. You will also learn how to compile the module, inspect kernel logs, and troubleshoot common errors.
A Linux kernel module is a piece of code that extends the functionality of the Linux kernel. It can be loaded into kernel memory when needed and removed when it is no longer required, subject to the module’s dependencies and whether it is currently in use.
Modules are commonly used for:
A compiled loadable kernel module commonly uses the .ko file extension, which stands for kernel object.
Consider a simple example. You write a module in C, compile it against the appropriate kernel build files, and produce a file named hello.ko. You then ask Linux to load it into the running kernel.
The module’s initialization function runs when the module loads successfully. When the module is removed, its exit function runs, allowing it to release resources or perform cleanup.
The process looks like this:
| Feature | Ordinary user-space program | Linux kernel module |
| Execution location | User space | Kernel space |
| Hardware access | Usually through system interfaces and drivers | Can implement kernel-level hardware functionality |
| Failure impact | Often limited to the process | May affect the entire operating system |
| Typical output | Terminal or application logs | Kernel log |
| Loading method | Run an executable | Load through kernel module tools |
| Programming interfaces | Standard user-space libraries and APIs | Kernel APIs and headers |
Before creating your first Linux kernel module, prepare a Linux environment with the required development tools.
You will need:
For beginners, Ubuntu running in a virtual machine is a practical choice. A virtual machine also helps reduce the risk of disrupting your primary operating system while experimenting with kernel code.
Open a terminal and execute:
uname -rThis command displays the version and build identifier of the running kernel.
For example, the output might resemble:
6.x.x-xx-genericYour actual output will depend on your Linux distribution and installed kernel.
Why does this matter? Kernel modules must be built for a compatible target kernel. The running kernel’s headers and build configuration are usually the correct starting point when building a module for that system.
On Ubuntu or Debian-based systems, run:
sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)The build-essential package provides commonly used compilation tools, including GCC and Make. The kernel headers package supplies files required to build external modules for the matching kernel.
Check that the kernel build directory exists:
ls -ld /lib/modules/$(uname -r)/buildIf the directory is missing, your distribution may not provide matching headers for the running kernel. You may need to install the appropriate kernel development package or boot into a kernel version for which matching build files are available.
Now let’s create a simple Linux kernel module example using C.
The module will print a message when it is loaded and another message when it is unloaded. It will not control hardware or modify system settings, making it a useful first exercise.
Run:
mkdir linux-kernel-module
cd linux-kernel-moduleCreate a source file named hello.c:
nano hello.cAdd the following code:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("IIES Student");
MODULE_DESCRIPTION("A simple Linux kernel module");
static int __init hello_init(void)
{
pr_info("Hello, Linux kernel module loaded!\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("Goodbye, Linux kernel module unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);Save the file and exit the editor.
Let’s understand the important parts of this code.
#include <linux/init.h>
#include <linux/module.h>The linux/init.h header provides initialization and cleanup annotations, including __init and __exit. The linux/module.h header provides declarations used for kernel modules, including module metadata and registration macros.
MODULE_LICENSE("GPL");
MODULE_AUTHOR("IIES Student");
MODULE_DESCRIPTION("A simple Linux kernel module");These macros describe the module. The license declaration is particularly important because the kernel uses module licensing information when determining whether certain kernel interfaces are available.
Use accurate licensing information for your actual code; do not declare a license that does not apply to your module.
static int __init hello_init(void)
{
pr_info("Hello, Linux kernel module loaded!\n");
return 0;
}The hello_init() function is called when the module is successfully initialized.
The pr_info() macro writes an informational message to the kernel log. It does not print directly to the terminal like printf() normally would in a user-space C program.
Returning 0 indicates successful initialization. A negative error code should be returned if initialization fails.
The __init annotation identifies initialization code that the kernel can discard after initialization has completed.
static void __exit hello_exit(void)
{
pr_info("Goodbye, Linux kernel module unloaded!\n");
}The hello_exit() function is called when the module is unloaded normally.
In a real device driver, this function might unregister a device, release resources, or stop operations started during initialization.
The __exit annotation identifies cleanup code. For a module built specifically as a loadable module, the exit function is used during normal unloading.
module_init(hello_init);
module_exit(hello_exit);module_init() identifies the initialization function, while module_exit() identifies the cleanup function for a loadable module.
Without the appropriate registration, the kernel would not know which functions to invoke for the module’s initialization and cleanup lifecycle.
Writing the C source file is only part of Linux kernel module development. You must also build it using the kernel’s external-module build system.
Create a file named Makefile in the same directory:
nano MakefileAdd the following:
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) cleanall: and clean: must begin with a real tab character, not spaces.obj-m += hello.o — This tells the kernel build system to build hello.o as an external module. The resulting loadable module will be named hello.ko.
KDIR — KDIR := /lib/modules/$(shell uname -r)/build points to the build directory for the currently running kernel.
M=$(PWD) — This identifies the directory containing your external module source and Makefile.
modules — This invokes the kernel build system’s module-building target.
clean — This removes generated build artifacts associated with the module build.
Using the kernel’s own build system is preferable to manually invoking GCC because kernel code depends on the target kernel’s configuration, compiler options, headers, and build conventions.
Once hello.c and Makefile are ready, run this command from the project directory:
makeThe Makefile invokes the kernel build system and compiles your source file.
If the build succeeds, the directory should contain a file named hello.ko.
You can check the generated files using:
ls -lDepending on the kernel and build configuration, you may also see intermediate files such as hello.o, hello.mod, hello.mod.c, or Module.symvers.
The kernel build process generally performs these steps:
.ko file.If compilation fails, read the first meaningful error in the output rather than focusing only on the final make failure message.
Common causes include missing kernel headers, a malformed Makefile, incorrect indentation, incompatible source code, and build files that do not match the intended kernel.
After compiling the module successfully, the next step is to load it into the running kernel.
Run:
sudo insmod hello.koThe insmod command attempts to insert the specified module into the running kernel.
If the command succeeds, it commonly produces no terminal output. That does not mean nothing happened; it means the insertion completed without reporting an error.
If the module cannot be loaded, inspect the error message and kernel logs.
Execute:
lsmod | grep helloIf the module is loaded, you should see an entry beginning with hello, followed by fields such as its size and usage count.
The lsmod command displays currently loaded modules. The grep hello portion filters the output to show lines containing the module name.
If nothing appears, check whether insertion failed, whether the module name differs from the expected name, or whether you are inspecting the correct system.
Run:
sudo dmesg | tail -n 20Look for a message similar to:
Hello, Linux kernel module loaded!The precise output depends on your system, and the message may not be visible at the end if other kernel messages were written afterward.
The dmesg command displays kernel ring-buffer messages. On some systems, access to these messages is restricted, which is why sudo may be needed.
A good kernel module tutorial should cover both loading and unloading. Cleanup is an essential part of kernel development because modules may allocate memory, register devices, or start operations that must be stopped before removal.
To remove the example module, execute:
sudo rmmod helloThe rmmod command removes the module named hello, provided that the kernel permits its removal.
Check the kernel log again:
sudo dmesg | tail -n 20You should find a message similar to:
Goodbye, Linux kernel module unloaded!You can also check whether the module remains loaded:
lsmod | grep helloIf the module was removed successfully, the command should return no matching entry.
These commands are frequently used in Linux kernel module programming.
| Command | Purpose |
insmod hello.ko | Inserts the specified module file into the kernel |
rmmod hello | Requests removal of a loaded module by name |
lsmod | Lists modules currently loaded in the kernel |
modprobe module_name | Loads a module by name and handles dependencies using the system’s module database |
modprobe -r module_name | Requests removal of a module and handles applicable dependency relationships |
dmesg | Displays kernel log messages, including messages produced by the example module |
One important distinction is that insmod expects a module file path, whereas modprobe typically resolves a module by name using the installed module tree and dependency information.
For modules installed in the system’s standard module directories, modprobe is generally more convenient because it can handle dependencies. For a freshly built external module in your current directory, insmod hello.ko is a straightforward demonstration.
Beginners often encounter errors when building or loading their first kernel module. Understanding the cause of each error is more useful than repeatedly running the same command.
You may see an error indicating that the kernel build directory cannot be found.
Possible cause: The headers or build files for the running kernel are not installed.
Solution on Ubuntu or Debian:
sudo apt update
sudo apt install linux-headers-$(uname -r)Then check:
ls -ld /lib/modules/$(uname -r)/buildIf the package is unavailable, check whether your distribution provides a matching kernel development package or whether you need to boot into a supported kernel version.
You may encounter an error such as:
Invalid module formatPossible cause: The module was built for a different kernel version, uses incompatible kernel configuration settings, or has another compatibility problem.
Solution: Rebuild the module against the correct kernel build files. Check the running kernel version and inspect the kernel log for a more specific explanation:
uname -r
sudo dmesg | tail -n 30The kernel’s version magic and other compatibility checks can prevent a module built for an incompatible kernel from loading.
The kernel may refuse to load a module even when you use sudo.
Possible causes include:
Solution: Inspect the kernel log and your distribution’s Secure Boot documentation. Some environments require modules to be signed with a trusted key. Do not disable security features without understanding the consequences.
When running rmmod, you may see an error indicating that the module is in use.
Possible cause: Another component depends on the module, or the module’s reference count prevents removal.
Solution: Inspect the module’s users and dependencies before attempting removal. Stop the dependent activity safely, and avoid forcing removal of a module that is actively being used.
You may load the module successfully but fail to find the message in your terminal.
Possible cause: The message was written to the kernel log rather than standard output.
Solution: Run:
sudo dmesg | tail -n 50Also verify that the module appears in lsmod. If your system restricts kernel-log access or uses a different logging configuration, consult its system logs.
Once the basic module works, try the following exercises to strengthen your understanding of Linux kernel module development.
Replace the initialization message with:
pr_info("IIES Linux kernel module is running!\n");Rebuild and reload the module. Remember to unload the previous version before inserting the new one.
Linux modules can accept parameters when loaded. Research the kernel’s module_param() and MODULE_PARM_DESC() macros, then add a simple integer parameter and print its value in the initialization function.
Use appropriate parameter permissions and types, and validate values when necessary. This exercise introduces configurable module behavior without requiring hardware.
Add a second log message to the initialization function and observe the order in which the messages appear. Then unload the module and confirm that the cleanup function runs.
Research how Linux device drivers interact with hardware through kernel APIs. Study concepts such as device registration, memory-mapped I/O, interrupts, and synchronization before attempting a hardware driver.
Do not access hardware registers directly without understanding the target device, its memory map, and the relevant kernel interfaces.
Investigate how external modules are built for a development board running embedded Linux. In cross-compilation environments, the build process must use the appropriate target kernel source or build tree, configuration, toolchain, and architecture.
A module built for your desktop Linux system should not be assumed to work on a different embedded board.
Linux kernel modules are relevant to embedded development because many embedded devices run Linux while relying on specialized hardware and configurable kernel functionality.
Examples include:
Learning to build and load a kernel module helps you understand the boundary between user-space applications and kernel-space functionality.
However, not every hardware feature requires a custom module. Linux already provides many drivers, and some devices are best accessed through existing kernel interfaces, user-space APIs, or established driver frameworks.
For students aiming for embedded Linux or device driver roles, the next learning steps include Linux system programming, kernel APIs, character device drivers, device trees, interrupts, synchronization, and cross-compilation.
Linux kernel modules provide a flexible way to extend kernel functionality without rebuilding the entire kernel for every supported feature. By creating a simple module in C, writing a Makefile, compiling it, loading it with insmod, inspecting its output using dmesg, and removing it with rmmod, you have learned the fundamentals of the module development workflow.
The most important lesson is that kernel programming differs from ordinary C programming. Kernel modules run with elevated privileges, depend on kernel interfaces, and require careful resource management and compatibility checks.
Practice the example in a virtual machine, understand each command, and gradually move toward module parameters and device driver development. These fundamentals will help you build a stronger foundation in Linux kernel programming and embedded Linux engineering.
insmod inserts a specified module file directly, while modprobe normally loads a module by name using the installed module database and handles dependencies. modprobe is generally more convenient for managing modules installed in the standard system module directories.
A Linux kernel module is a piece of code that extends the functionality of the Linux kernel. It can be loaded when needed and removed when no longer required, making it useful for device drivers, networking, and other supported kernel features.
Write the module in C, create a Makefile using the kernel build system, and run make to generate the .ko file. You can then load it using sudo insmod hello.ko, verify it with lsmod, and inspect its output using dmesg.
insmod inserts a specified module file directly, while modprobe normally loads a module by name using the installed module database and handles dependencies. modprobe is generally more convenient for managing modules installed in the standard system module directories.
Indian Institute of Embedded Systems – IIES