Linux Kernel Module Stacking: EXPORT_SYMBOL, Module Dependencies, and Library-Style LKMs
When you write a Linux kernel module (LKM), you are writing a self-contained piece of code that the kernel can load and unload at runtime. But what happens when two or more kernel modules need to share functions or data structures with each other? That is where linux kernel module stacking and the EXPORT_SYMBOL() macro become essential tools in your kernel programming toolkit.
In this tutorial, we cover everything you need to know about linux kernel module stacking — from understanding what symbol exporting means, to building a real proof-of-concept with a core LKM and a consumer LKM, to inspecting module dependencies with lsmod. By the end, you will be able to design multi-module kernel projects the same way professional kernel developers do.
This is part of the EmbeddedPathashala free linux device drivers course — a completely free linux kernel development course for embedded engineers and students.
What You Will Learn
- What
EXPORT_SYMBOL()andEXPORT_SYMBOL_GPL()do and why they matter - How linux kernel module stacking works conceptually and in practice
- How to read
lsmodoutput to understand module dependencies and usage counts - How to build a two-module stacked project: a core (library) LKM and a consumer LKM
- How the kernel’s module reference counting prevents unsafe unloading
- Real-world examples of module stacking (VirtualBox, LTTng)
- Common mistakes to avoid when exporting symbols across kernel modules
Prerequisites
- Basic understanding of writing and loading a simple kernel module (
insmod/rmmod) - Familiarity with C — especially
externdeclarations and function pointers - A Linux system running kernel 6.x (or 5.x) with kernel headers installed
- A working cross-compilation or native build environment
If you are brand new to kernel modules, start with our Introduction to Linux Kernel Modules lesson first, then come back here.
What Is EXPORT_SYMBOL() and Why Does the Kernel Need It?
Normally, a C function or variable declared in one .c file can be used in another .c file within the same binary — you simply declare it with extern and the linker resolves it at build time. The kernel module world is different. Each LKM is a separate relocatable object file that gets linked into the running kernel at load time, not at compile time.
That means a function defined inside module_A.ko is invisible to module_B.ko by default — even if both are loaded. To make a symbol (function or variable) visible across module boundaries, the kernel provides a dedicated macro:
EXPORT_SYMBOL(symbol_name);
EXPORT_SYMBOL_GPL(symbol_name);
Placing either macro right after the definition of a function or variable tells the kernel to put that symbol into a special in-kernel table called the Exported Symbol Table (sometimes called the kernel symbol table). Any subsequently loaded module can then resolve references to that symbol at load time.
|
core_lkm.ko — Library Module
void llkd_sysinfo2(void) {
/* prints kernel info */ } EXPORT_SYMBOL_GPL(llkd_sysinfo2); int llkd_data = 42; EXPORT_SYMBOL_GPL(llkd_data); |
⇄ |
user_lkm.ko — Consumer Module
extern void llkd_sysinfo2(void);
extern int llkd_data; static int __init user_lkm_init() { llkd_sysinfo2(); pr_info(“%d”, llkd_data); return 0; } |
|
KERNEL EXPORTED SYMBOL TABLE
|
EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL
There are two variants of the macro and the difference matters if you ever plan to distribute your module:
| Macro | Who Can Use the Symbol? | Typical Use Case |
|---|---|---|
EXPORT_SYMBOL() |
Any kernel module, regardless of license | General-purpose APIs |
EXPORT_SYMBOL_GPL() |
Only modules whose MODULE_LICENSE() includes “GPL” |
Core kernel subsystems, driver frameworks |
EXPORT_SYMBOL_GPL() is used extensively in the kernel itself — on a 6.x kernel tree you will find tens of thousands of usages. It is the kernel community’s way of ensuring that proprietary modules cannot freely use GPL-licensed internal APIs.
Pro tip: To view every exported symbol in your kernel tree, run the following command from the root of the kernel source after building it:
make export_report
Understanding Linux Kernel Module Stacking
Linux kernel module stacking is an architectural pattern where one or more “core” kernel modules act as libraries, exporting symbols that other modules depend on. The dependent modules are figuratively (and literally, in lsmod output) stacked on top of the core module.
This pattern lets you:
- Avoid duplicating common code across multiple modules
- Enforce a clean separation between shared infrastructure and per-device/subsystem logic
- Build large kernel subsystems incrementally, loading only the components that are needed
Reading lsmod to Understand Module Dependencies
The lsmod command shows you three columns: module name, size in bytes, and usage count. When a usage count is greater than zero, there may be extra module names listed after the third column. Those extra names are the modules that depend on (i.e., are stacked on top of) the module on the left.
Here is a real example from an Ubuntu host running Oracle VirtualBox:
$ lsmod | grep vbox
vboxnetadp 28672 0
vboxnetflt 28672 1
vboxdrv 479232 3 vboxnetadp,vboxnetflt
Read this output as follows: vboxnetadp and vboxnetflt both depend on vboxdrv. The vboxdrv module is the core “library” — its usage count is 3, and two of those references come from the two dependent modules stacked on it. The third reference likely comes from a running process or thread.
|
vboxnetadp.ko
Dependent module
usage count: 0
|
vboxnetflt.ko
Dependent module
usage count: 1
|
EXPORT_SYMBOL(vbox_api_destroy);
EXPORT_SYMBOL(vbox_idc_connect);
This is linux kernel module stacking in action. The key rule that follows directly from this architecture: you cannot rmmod a module while its usage count is greater than zero. To remove vboxdrv you must first remove vboxnetadp and vboxnetflt. Only then does the usage count drop to zero and rmmod vboxdrv succeeds.
A Large-Scale Example: LTTng Module Stacking
The LTTng (Linux Tracing Toolkit next generation) framework is an even more dramatic example of linux kernel module stacking. When LTTng is installed, it loads 40 or more kernel modules. The lttng_tracer core module alone has 35 modules stacked on top of it, and lttng_lib_ring_buffer has 23 dependents. Running:
lsmod | grep --color=auto "^lttng"
shows the full dependency tree. You can also quickly list all modules with a non-zero usage count with this one-liner:
lsmod | awk '$3 > 0 {print $0}'
How the Kernel Tracks Module Usage: Reference Counting
The usage count you see in lsmod is not just a display number — it is backed by a 32-bit atomic variable inside the kernel’s module data structure. Every time a module is loaded that depends on another, the kernel atomically increments the dependency’s usage count. When the dependent module is removed, the count is decremented.
This is a classic reference counter pattern, the same idea used in Linux file descriptors, network socket buffers, and many other kernel subsystems. Using an atomic variable ensures that concurrent loads and unloads from different CPU cores do not produce a race condition and corrupt the count.
| Event | Effect on Core Module’s Usage Count |
|---|---|
Dependent module loaded with insmod |
Incremented by 1 |
Dependent module removed with rmmod |
Decremented by 1 |
| A running process/thread uses the module | Incremented by 1 (process-held reference) |
rmmod attempted on core module (count > 0) |
Fails with EBUSY — module is still in use |
rmmod attempted on core module (count = 0) |
Succeeds — module safely unloaded |
Hands-On: Building a Stacked Kernel Module Pair
Let us build a minimal but complete linux kernel module stacking demonstration. We will create two modules:
- core_lkm — the library module that exports a function and a variable
- user_lkm — the consumer module that uses those exported symbols
Step 1: The Core (Library) Module — core_lkm.c
The core module defines and exports two symbols: a function called llkd_sysinfo2() and an integer variable called exp_int. Notice that we do not mark exported symbols as static — doing so with modern toolchains will produce a warning.
// core_lkm.c — the "library" kernel module
// Part of EmbeddedPathashala free linux kernel development course
#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Core LKM – demonstrates linux kernel module stacking");
MODULE_LICENSE("Dual MIT/GPL");
/* -------------------------------------------------------
* Exported variable
* -------------------------------------------------------*/
int exp_int = 200;
EXPORT_SYMBOL_GPL(exp_int);
/* -------------------------------------------------------
* Exported function
* Notice: NOT declared static — required for exported symbols
* -------------------------------------------------------*/
void llkd_sysinfo2(void)
{
char buf[16];
/* snprintf is safe to use in kernel context */
snprintf(buf, sizeof(buf), "%d.%d.%d",
(LINUX_VERSION_CODE >> 16) & 0xFF,
(LINUX_VERSION_CODE >> 8) & 0xFF,
(LINUX_VERSION_CODE) & 0xFF);
pr_info("Linux version : %s\n", buf);
pr_info("CPU : %u logical CPUs online\n", num_online_cpus());
}
EXPORT_SYMBOL_GPL(llkd_sysinfo2);
static int __init core_lkm_init(void)
{
pr_info("core_lkm loaded — symbols exported and ready\n");
return 0;
}
static void __exit core_lkm_exit(void)
{
pr_info("core_lkm unloaded\n");
}
module_init(core_lkm_init);
module_exit(core_lkm_exit);
Key points in this module:
EXPORT_SYMBOL_GPL(llkd_sysinfo2)— placed immediately after the function definition, this puts the symbol into the kernel’s exported symbol table with GPL restriction.EXPORT_SYMBOL_GPL(exp_int)— variables can be exported just like functions.- The module uses
Dual MIT/GPLas its license so it can itself use GPL-exported symbols from the kernel.
Step 2: The Consumer Module — user_lkm.c
The consumer module uses extern declarations to tell the C compiler that llkd_sysinfo2 and exp_int exist somewhere else. The kernel module loader resolves these at insmod time using the exported symbol table.
// user_lkm.c — the "consumer" kernel module
// Part of EmbeddedPathashala free linux device drivers course
#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("User LKM – consumes symbols from core_lkm (module stacking demo)");
MODULE_LICENSE("Dual MIT/GPL");
/* Extern declarations — the compiler needs to know about these.
* The actual resolution happens at module load time via the
* kernel's exported symbol table. */
extern void llkd_sysinfo2(void);
extern int exp_int;
static int __init user_lkm_init(void)
{
pr_info("user_lkm loaded — calling into core_lkm\n");
/* Call the function exported by core_lkm */
llkd_sysinfo2();
/* Read the variable exported by core_lkm */
pr_info("exp_int value from core_lkm = %d\n", exp_int);
return 0;
}
static void __exit user_lkm_exit(void)
{
pr_info("user_lkm unloaded\n");
}
module_init(user_lkm_init);
module_exit(user_lkm_exit);
Step 3: The Makefile
Both modules must be built by a single Makefile. The obj-m variable lists both module names. Each module gets its own <name>-objs line if it consists of more than one source file (here each is a single file, so the line is optional but explicit).
# Makefile for linux kernel module stacking demo
# Adjust KDIR for cross-compilation (e.g., for ARM targets)
KDIR ?= /lib/modules/$(shell uname -r)/build
obj-m += core_lkm.o
obj-m += user_lkm.o
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
For cross-compilation targeting an ARM board running Linux 6.x, set KDIR to your cross-compiled kernel build tree and export the correct ARCH and CROSS_COMPILE:
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
make KDIR=/path/to/arm64-kernel-build
Step 4: Loading in the Correct Order
Module stacking has a strict load order requirement: the core (library) module must be loaded before any module that depends on it. Attempting to load user_lkm first will fail because the kernel cannot resolve llkd_sysinfo2 — it is not yet in the exported symbol table.
# Load order — core FIRST
sudo insmod ./core_lkm.ko
sudo insmod ./user_lkm.ko
# Verify with lsmod
lsmod | grep -E "core_lkm|user_lkm"
# Expected output:
# user_lkm 16384 0
# core_lkm 16384 1 user_lkm
# Check kernel log
dmesg | tail -10
After loading, notice that core_lkm shows a usage count of 1 with user_lkm listed as its dependent — this is linux kernel module stacking working exactly as designed.
Step 5: Unloading in Reverse Order
Unloading must happen in reverse order — the dependent module first, then the core:
# Unload order — consumer FIRST, core LAST
sudo rmmod user_lkm
sudo rmmod core_lkm
If you try to rmmod core_lkm while user_lkm is still loaded, the kernel will refuse with:
rmmod: ERROR: Module core_lkm is in use by: user_lkm
Common Mistakes with Linux Kernel Module Stacking
| Mistake | Symptom | Fix |
|---|---|---|
Loading user_lkm before core_lkm |
insmod: ERROR: could not insert module: Unknown symbol |
Always load the core module first |
Marking exported symbol as static |
Compiler warning; symbol may not be visible | Never use static on exported symbols |
Forgetting extern declaration in consumer |
Implicit function declaration warning / build error in C | Always declare extern in consumer module |
Mismatched MODULE_LICENSE() with EXPORT_SYMBOL_GPL() |
insmod fails: symbol requires GPL license |
Set MODULE_LICENSE("GPL") or "Dual MIT/GPL" in consumer |
Trying to rmmod core while consumer is loaded |
ERROR: Module is in use |
Always unload dependents first |
| Header-only approach (no EXPORT_SYMBOL) | Linker error at insmod time |
You must use EXPORT_SYMBOL() — headers alone are not enough |
Best Practices for Linux Kernel Module Stacking
- Keep exported APIs minimal. Only export what truly needs to be shared. Every exported symbol is a public interface that other modules can depend on — changing it later becomes an ABI compatibility concern.
- Use EXPORT_SYMBOL_GPL() for new code. This is the kernel community’s preferred choice and ensures your code only gets used by GPL-compatible modules.
- Use a shared header file. Place your
externdeclarations and any shared data structure definitions in a common header (e.g.,core_lkm.h) and include it in both modules. This avoids duplication and keeps declarations synchronized. - Document the load order. Especially for production drivers, document in your README which modules must be loaded first and consider using
modprobewith a propermodules.depdependency entry so the kernel handles ordering automatically. - Use modprobe over insmod.
modprobereads/lib/modules/$(uname -r)/modules.depand automatically loads dependencies in the correct order. During development,insmodis fine, but production systems should usemodprobe. - Test unload paths carefully. Always verify that your module’s
exitfunction properly undoes everything theinitfunction did, so that repeated load/unload cycles do not leak memory or kernel objects.
Real-World Linux Kernel Module Stacking Use Cases
Understanding linux kernel module stacking is not just an academic exercise — it is used pervasively in production Linux systems:
- Filesystem stacking (overlay filesystems). Docker’s OverlayFS driver depends on the core VFS (Virtual Filesystem Switch) layer through exported symbols.
- DRM/GPU drivers. The
drm.kocore module exports hundreds of symbols consumed by vendor-specific GPU modules likei915.ko,amdgpu.ko, andnouveau.ko. - USB subsystem.
usbcore.kois the library module. Every USB device driver (HID, storage, networking) is stacked on top of it. - Bluetooth.
bluetooth.koexports the core HCI and L2CAP APIs. Protocol-specific modules likerfcomm.ko,bnep.ko, andhidp.koare stacked on it — which is exactly the architecture used in BlueZ. - Network drivers. Many NIC drivers depend on
libphy.ko(PHY layer library) through module stacking.
Performance and Security Considerations
Performance
Calling a function through an exported symbol has essentially the same overhead as a normal function call within the kernel — there is no dynamic dispatch or vtable lookup involved. The symbol address is resolved at module load time and written directly into the consumer module’s code/data, so the runtime cost is negligible.
The main performance consideration is the module load time: loading a module that has many symbol dependencies requires the kernel to iterate the exported symbol table once for each unresolved reference. For a module with hundreds of dependencies (like some DRM drivers), this can take a few milliseconds longer than loading a standalone module.
Security
- With
CONFIG_MODULE_SIGenabled (mandatory in many distros), all modules — including stacked ones — must be signed with a trusted key. An unsigneduser_lkm.kowill be rejected even ifcore_lkm.kois properly signed. EXPORT_SYMBOL_GPL()adds a layer of license enforcement: proprietary modules cannot use GPL-only symbols, reducing the risk of proprietary code silently depending on GPL infrastructure.- Verify your loaded modules with:
cat /proc/modulesorlsmod— any unexpected module appearing in the dependency chain is worth investigating.
Summary and Key Takeaways
EXPORT_SYMBOL()andEXPORT_SYMBOL_GPL()put kernel symbols into a global table that other modules can resolve at load time.- Linux kernel module stacking is the design pattern where a “core” module acts as a library, exporting symbols consumed by one or more dependent modules stacked on top of it.
- The
lsmodthird column (usage count) and the modules listed after it are the primary tools for inspecting the stacking hierarchy. - The kernel enforces correct unload order through an atomic reference counter — a module with a non-zero usage count cannot be removed.
- Real-world subsystems — USB, DRM, Bluetooth, filesystems — all rely heavily on linux kernel module stacking.
- Always load the core module before the consumer, and unload the consumer before the core.
Conclusion
Linux kernel module stacking is one of the most practical and widely-used architectural patterns in kernel development. Once you understand how EXPORT_SYMBOL() works and how the kernel tracks module dependencies through reference counting, you can design multi-module kernel projects cleanly — separating shared infrastructure from specific logic, enabling modular builds, and making your driver code maintainable.
The two-module proof-of-concept we built here — core_lkm and user_lkm — is the exact same pattern you will find in production-grade kernel subsystems. Study it, extend it, and refer back to it as you explore larger projects.
This tutorial is part of the EmbeddedPathashala free linux kernel development course. Continue to the next lesson on Kernel Module Parameters and sysfs Integration, or explore our free linux device drivers course index for the full learning path.
Authoritative References
- The Linux Kernel API Documentation (kernel.org)
- Building External Modules — Linux Kernel Documentation
- LTTng (Linux Tracing Toolkit next generation) Documentation
- Linux Kernel Licensing Rules (GPL and EXPORT_SYMBOL_GPL)
Frequently Asked Questions (FAQ)
Q1. What is linux kernel module stacking?
Linux kernel module stacking is an architectural pattern where one or more kernel modules export symbols (functions and variables) that other kernel modules depend on. The dependent modules are described as “stacked” on the core module. This is the standard way to implement shared, library-style functionality inside the Linux kernel without hard-coding it into a single monolithic module.
Q2. What is the difference between EXPORT_SYMBOL and EXPORT_SYMBOL_GPL?
EXPORT_SYMBOL() makes a symbol available to any kernel module regardless of its license. EXPORT_SYMBOL_GPL() restricts access to modules that declare a GPL-compatible license in their MODULE_LICENSE() macro. The vast majority of new kernel code uses EXPORT_SYMBOL_GPL().
Q3. Why does lsmod show a usage count for my module?
A non-zero usage count means something is currently using your module — either another module is stacked on it, or a process/thread is actively calling into it. The usage count is implemented as a 32-bit atomic reference counter inside the kernel’s module infrastructure.
Q4. Can I remove a core module while dependent modules are still loaded?
No. The kernel will refuse the rmmod with an “in use” error. You must first remove all modules stacked on top of the core, bringing its usage count to zero, before the core module itself can be removed.
Q5. Do I need to use extern declarations in my consumer module?
Yes, always. Placing a symbol in the exported symbol table with EXPORT_SYMBOL() handles the runtime linkage, but the C compiler still needs a declaration at compile time to know the type signature of the function or variable. Use a shared header file or explicit extern declarations in the consumer source file.
Q6. Can I use EXPORT_SYMBOL on a static function?
You should not. Marking an exported function or variable as static makes its scope file-local at the C language level, which conflicts with the intent of exporting it. Modern toolchains may issue a warning. Simply drop the static keyword from any symbol you plan to export.
Q7. How does modprobe handle module stacking automatically?
modprobe reads the /lib/modules/$(uname -r)/modules.dep file, which is generated by depmod and lists every module’s dependencies. When you run modprobe user_lkm, it automatically loads core_lkm first. This is the preferred approach for production systems.
Q8. What happens if a symbol name collision occurs between two core modules?
The kernel will print a warning during module load about a duplicate symbol and the second module’s symbol will shadow the first. This is a real problem in practice and is why kernel module authors should namespace their exported symbols carefully — for example, by prefixing them with a subsystem or project name (e.g., vbox_, lttng_, ep_).
Q9. Is linux kernel module stacking available on all architectures?
Yes. Module stacking is a core feature of the Linux module loader and works on every architecture that Linux supports — x86, ARM, ARM64, RISC-V, MIPS, PowerPC, and others.
Q10. Where can I find a free linux kernel development course that covers module stacking in depth?
EmbeddedPathashala offers a completely free linux kernel development course and free linux device drivers course covering this topic and much more — from basic module writing all the way to advanced driver frameworks. Visit embeddedpathashala.com to access all lessons at no cost.
Continue Your Free Linux Kernel Development Journey
This tutorial is part of EmbeddedPathashala’s completely free linux device drivers course and free linux kernel development course — no sign-up required.
Browse All Free Courses Next Lesson →