Topics Covered:
Why use kernel modules
.ko file format
insmod utility
Kernel VAS
Out-of-tree code
Dynamic kernel extension
Before We Begin: A Quick Recap
In the previous sections, we looked at how the Linux kernel divides memory into two regions — user space and kernel space. We also understood that these are virtual address spaces, not physical memory. The kernel uses a master page table to map its own virtual pages to physical frames, and each running process gets its own page table.
Now it is time to ask a very practical question: if I want to add new functionality to the Linux kernel — say, a device driver for a custom chip — what is the best way to do it? This is exactly what the LKM framework answers.
The Problem: Adding Code to the Kernel the Hard Way
Imagine your company just designed a new hardware sensor. The Linux kernel does not know how to talk to it. Someone needs to write a driver. The obvious approach would be to directly edit the kernel source code, add the driver there, recompile the entire kernel, and then reboot the machine to test it.
This sounds fine at first, but think about what happens in practice:
| ✎ Edit kernel source Add your driver code |
→ | ▶ Rebuild entire kernel make -j$(nproc) — takes 20-40 mins |
→ | ↻ Reboot machine Lose all running processes |
| ⚠ One tiny bug fix? Repeat the entire process again and again… | ||||
Even a one-line bug fix forces you to rebuild the whole kernel image and reboot. On a production system or an embedded board, this is painful and impractical. There has to be a smarter way — and that is exactly what the Loadable Kernel Module (LKM) framework provides.
What is a Loadable Kernel Module (LKM)?
A kernel module is a piece of kernel-level code that you write and compile outside the kernel source tree. You do not need to touch or rebuild the main kernel. Instead, you compile your code into a special binary file and then load it directly into the running kernel — while the system is live. When you are done with it, you can unload it just as easily.
rmmod mydriver
Module is cleanly
unloaded from memory
Think of it like a USB pendrive for the kernel. You plug it in when you need it, the kernel recognizes and uses it, and you pull it out when you’re done. No reboot needed.
The .ko File: What Does a Kernel Module Look Like?
When you build a kernel module, the output is a file with the .ko extension — which stands for Kernel Object. This is a binary file, but it is not an executable like the programs you run in user space. It is an ELF (Executable and Linkable Format) relocatable object that the kernel knows how to link into itself at load time.
In the older Linux 2.4 era, kernel module files used the plain .o extension (object file). Starting with Linux 2.6 (circa 2003 and onwards), the extension changed to .ko to clearly distinguish kernel objects from ordinary userspace object files. Every modern Linux system — including the current 6.x kernels — uses .ko (and sometimes .ko.xz or .ko.zst for compressed modules).
On a modern Ubuntu or Fedora system running kernel 6.x, you can see installed kernel modules like this:
# Check your running kernel version
$ uname -r
6.8.0-45-generic
# Count how many .ko modules are installed for your kernel
$ find /lib/modules/$(uname -r)/ -name "*.ko*" | wc -l
# On Ubuntu 24.04 with kernel 6.8, you might see 6000+ modules
# The .ko.zst extension is common now — modules are zstd-compressed by default
Starting around kernel 5.13+, distributions began shipping modules compressed with zstd (.ko.zst) or xz (.ko.xz) to save disk space. The kernel automatically decompresses them when loading. When you build your own out-of-tree modules, the output is still a plain .ko file — compression is applied by package managers during installation.
How the LKM Framework Works: The Big Picture
Let’s walk through the complete lifecycle of a kernel module — from source code to running inside the kernel.
Invokes kernel build system
Key Advantages of the LKM Approach
Load, test, fix, and reload your module — all while the system keeps running. Development cycles that used to take 30 minutes now take 30 seconds.
Embedded products can ship with a set of modules that get loaded based on detected hardware or product tier. No need to bake everything into the kernel image.
You can write a diagnostic module, load it on a production device, collect logs, and remove it — without disturbing the running system. Tools like kprobes rely on this.
Not every driver needs to be compiled into the base kernel. Uncommon drivers ship as modules and are only loaded when the hardware is detected, keeping the base image lean.
What LKMs Cannot Do
Not every part of the kernel can be modularized. Certain core subsystems are so fundamental that they must be compiled directly into the kernel binary and cannot be provided as loadable modules. These include:
- The core CPU scheduler
- Memory management core paths
- Core interrupt and exception handling
- The timer subsystem core
- Core signaling infrastructure
Additionally, a kernel module can only access a subset of the full kernel API. Functions and data structures that are not explicitly exported by the kernel are not accessible from modules. We will explore EXPORT_SYMBOL and the module symbol table in a later tutorial.
Is Linux Monolithic or Modular?
This is a question that confuses many students. Linux is often called a monolithic kernel — meaning all kernel services run in a single address space at ring 0, unlike a microkernel where services run in separate user-mode processes.
However, Linux is also modular thanks to the LKM framework. The correct answer is that Linux is a modular monolithic kernel. The core structure is monolithic (one address space, full privilege), but the LKM framework allows dynamic extension of that kernel at runtime.
How Do You Load a Module? Meet insmod and modprobe
Once you have compiled your .ko file, you use one of two utilities to load it:
Loads a specific .ko file directly. You must give it the exact path. It does not automatically resolve dependencies.
$ sudo insmod ./mydriver.ko
Best for testing your own modules during development.
Loads a module by name and automatically loads any modules it depends on first. Looks modules up from /lib/modules/$(uname -r)/.
$ sudo modprobe e1000e
Used by the system for automatic hardware detection.
To remove a loaded module, use rmmod or modprobe -r:
# Remove a module you loaded with insmod
$ sudo rmmod mydriver
# Remove a module loaded by modprobe (also removes unused dependencies)
$ sudo modprobe -r e1000e
A Crucial Point: Your Module Runs in Kernel Space
When you write a kernel module and load it, your code does not run as a user program. It becomes part of the kernel itself. This means:
- No memory protection: A bad pointer dereference won’t give you a segfault — it will likely hang or crash the kernel (kernel oops or panic).
- No standard C library: You cannot use
printf(),malloc(), or any glibc function. You use kernel equivalents likeprintk()andkmalloc(). - Full CPU privilege: Your code runs at ring 0 on x86. It can access any memory address, any hardware register.
- No floating point by default: The kernel does not save/restore FPU state on context switches by default. Avoid float in modules unless you explicitly save FPU state.
🎓 Interview Questions & Answers
A Linux Kernel Module (LKM) is a piece of kernel code compiled separately from the main kernel source tree. It can be dynamically loaded into or removed from a running kernel without rebooting. This is useful because it avoids the rebuild-and-reboot cycle every time you need to add or update kernel functionality like a device driver.
Kernel modules have a .ko (Kernel Object) extension. The file is an ELF relocatable binary — not a standalone executable. It contains compiled kernel code that the kernel links into its own address space when loaded. On compressed systems, you may see .ko.xz or .ko.zst.
insmod loads a module directly from the given file path and does not resolve dependencies automatically. modprobe loads a module by name, automatically loading any required dependency modules first, using the module database in /lib/modules/$(uname -r)/. In development, insmod is convenient; in production systems, modprobe is preferred.
Linux is a modular monolithic kernel. It is monolithic in the sense that all kernel services — scheduler, memory management, drivers, filesystems — run in the same address space at full CPU privilege. It is modular because the LKM framework allows kernel functionality to be added and removed dynamically without rebuilding the kernel. This combines the performance benefits of a monolithic design with the flexibility of modularity.
No. Kernel modules do not have access to the C standard library (glibc). Instead, you use kernel equivalents: printk() for printing (output goes to the kernel ring buffer, readable via dmesg), and kmalloc() / kzalloc() for dynamic memory allocation. The kernel provides its own rich set of APIs for everything a module needs.
Since a kernel module runs in kernel space at ring 0, a bug can be catastrophic. A NULL pointer dereference, for example, will not be caught by user-space memory protection — it will cause a kernel oops or a kernel panic (system crash). This is why module development requires careful coding, testing in virtual machines first, and using tools like KASAN (Kernel Address Sanitizer) in kernel 6.x to catch memory bugs early.
No. Core features that are integral to the kernel’s basic operation — such as the CPU scheduler core, memory management core, interrupt handling, and the timer subsystem — must be compiled into the kernel itself. The LKM framework is best suited for device drivers, filesystems, network protocols, and similar add-on functionality. Also, a module can only call kernel functions that are explicitly exported via EXPORT_SYMBOL or EXPORT_SYMBOL_GPL.
Continue Learning — Free Linux Kernel Development Course
Next up: Writing Your First Hello World Kernel Module from scratch, understanding the Makefile, and running it live.
