Linux Kernel Module Stacking
How modules share code using EXPORT_SYMBOL, inter-module dependencies, and loading order — updated for Linux 6.x
What You Will Learn
This lecture covers one of the most practical and commonly-used features in Linux kernel module development — the ability for one kernel module to call functions defined in another module. This is called module stacking or inter-module dependencies.
By the end of this lecture you will understand:
Prerequisites
Before reading this lecture you should already know:
- How to write a basic kernel module with
module_init/module_exit - How to build a module using a Makefile and
make -C /lib/modules/$(uname -r)/build - What
insmod,rmmod,lsmod, anddmesgdo - What the
MODULE_LICENSEandMODULE_AUTHORmacros do
If any of these are unfamiliar, go back to the earlier lectures in this free Linux kernel programming course first.
What Is Module Stacking?
In large software systems it makes sense to split code into layers. The Linux kernel does exactly this — even for loadable kernel modules. One module can export its functions and variables so that another module can call them at runtime. This arrangement is called module stacking.
Think of it like a shared library in user-space. In user-space, many programs can link against libc.so without each carrying their own copy of malloc. Module stacking gives you the same benefit inside the kernel.
|
user_module.ko (consumer) calls functions exported by provider_module |
| ↓ depends on |
|
provider_module.ko (base / core) exports functions with EXPORT_SYMBOL or EXPORT_SYMBOL_GPL |
| ↓ sits on top of |
|
Linux Kernel Core (vmlinux) always present — all modules run in kernel space |
Real kernel subsystems use this all the time. The USB stack is a classic example: usbcore.ko is the base module that handles USB host controller details. Device-specific drivers like usb-storage.ko or usbhid.ko stack on top and call functions provided by usbcore. Neither has to duplicate the USB enumeration logic.
In the ALSA audio subsystem, snd.ko is the core. Modules like snd_pcm.ko, snd_timer.ko, and hardware-specific modules like snd_hda_intel.ko all depend on it. This is module stacking in production.
How a Module Exports a Symbol
By default, a function or variable defined in a kernel module is visible only to code inside that same module. To make something available to other modules you must explicitly export it using one of these two macros:
EXPORT_SYMBOL(symbol_name);
EXPORT_SYMBOL_GPL(symbol_name);
You place these macros at file scope, right after the function or variable definition. They cannot go inside a function body.
Here is a working provider module for Linux 6.x:
// provider_module.c
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Provider module - exports functions for other modules");
/* A simple exported function - available to any module */
int ep_add(int a, int b)
{
pr_info("ep_add called: %d + %d\n", a, b);
return a + b;
}
EXPORT_SYMBOL(ep_add);
/* A GPL-only exported function - only GPL modules can use this */
void ep_show_platform_info(void)
{
#if BITS_PER_LONG == 64
pr_info("Platform: 64-bit kernel\n");
#else
pr_info("Platform: 32-bit kernel\n");
#endif
}
EXPORT_SYMBOL_GPL(ep_show_platform_info);
static int __init provider_init(void)
{
pr_info("provider_module: loaded\n");
return 0;
}
static void __exit provider_exit(void)
{
pr_info("provider_module: unloaded\n");
}
module_init(provider_init);
module_exit(provider_exit);
And a consumer module that calls those functions:
// consumer_module.c
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Consumer module - depends on provider_module");
/* Tell the compiler these symbols exist externally */
extern int ep_add(int a, int b);
extern void ep_show_platform_info(void);
static int __init consumer_init(void)
{
int result;
pr_info("consumer_module: loaded\n");
result = ep_add(10, 25);
pr_info("consumer_module: ep_add(10, 25) = %d\n", result);
ep_show_platform_info();
return 0;
}
static void __exit consumer_exit(void)
{
pr_info("consumer_module: unloaded\n");
}
module_init(consumer_init);
module_exit(consumer_exit);
The extern declarations tell the C compiler that these symbols are defined somewhere else. At kernel module load time, the kernel’s symbol resolution code links these names to the real addresses inside the already-loaded provider module.
EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL — What Is the Difference?
| Feature | EXPORT_SYMBOL | EXPORT_SYMBOL_GPL |
|---|---|---|
| Who can use it? | Any module, regardless of license | Only GPL-compatible modules |
| Intended for | Generic utility functions, stable APIs | Core kernel internals deeply tied to GPL code |
| Build-time check | No restriction | modpost errors if consumer is non-GPL |
| Load-time check | No restriction | Kernel refuses to load non-GPL module |
| Example symbols | printk, kmalloc, kfree |
__rcu_read_lock, kernel_neon_begin |
The kernel checks your module’s MODULE_LICENSE string against a known list of GPL-compatible values. The strings the kernel accepts as GPL-compatible are:
"GPL" /* GNU Public License v2 or later */
"GPL v2" /* GNU Public License v2 only */
"GPL and additional rights"
"Dual BSD/GPL"
"Dual MIT/GPL"
"Dual MPL/GPL"
Any other string — including "Proprietary" — is treated as non-GPL. The kernel loads the module but marks the kernel as tainted with the P flag, and the module cannot access any EXPORT_SYMBOL_GPL symbol.
The Kernel Symbol Table — /proc/kallsyms and Module.symvers
The kernel maintains a live table of all known symbols — from the core kernel and from every loaded module. You can inspect this through /proc/kallsyms:
$ sudo grep ep_add /proc/kallsyms
ffffffffc05a1000 T ep_add [provider_module]
Each line has four fields: address, type letter, symbol name, and optionally the owning module in brackets. An uppercase type letter means the symbol is globally visible (exported). A lowercase letter means it is local (not exported). The letter T or t means the symbol lives in the text (code) section.
| Letter | Meaning | Exported? |
|---|---|---|
T | Text (function) — global | Yes |
t | Text (function) — local | No |
D | Initialized data — global | Yes |
d | Initialized data — local | No |
B | Uninitialized (BSS) data — global | Yes |
b | Uninitialized (BSS) data — local | No |
Module.symvers is a file generated during the kernel or module build. It lists every exported symbol with a CRC checksum used for ABI version checking. When you build an out-of-tree module that depends on another out-of-tree module, you may need to provide that module’s Module.symvers to the build system so the linker can resolve and version-check symbols correctly.
Building Two Modules Together — The Makefile
When you have two modules in the same directory that need to be built together, the Makefile lists both:
obj-m := provider_module.o
obj-m += consumer_module.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
After running make you will have two .ko files. The build tool modpost runs automatically during the build and checks that all symbols the consumer needs are actually exported by the provider. If you forget EXPORT_SYMBOL, the build will warn you at this stage, before you even try to load anything.
Loading Order — Why It Matters
This is where many beginners get confused. The kernel cannot load a module if any symbol it needs is not yet available in the kernel’s symbol table. The provider module must be loaded first.
With insmod, you control the order manually:
# Step 1: load the provider first
$ sudo insmod ./provider_module.ko
# Step 2: now load the consumer
$ sudo insmod ./consumer_module.ko
If you get it backwards:
$ sudo insmod ./consumer_module.ko
insmod: ERROR: could not insert module ./consumer_module.ko: Unknown symbol in module
$ dmesg | tail
[12345.678] consumer_module: Unknown symbol ep_add (err -2)
[12345.679] consumer_module: Unknown symbol ep_show_platform_info (err -2)
The error Unknown symbol means the kernel tried to find ep_add in its symbol table and could not. The provider has not been loaded yet, so the symbol does not exist. The load fails completely — nothing gets inserted.
With modprobe, the kernel tools handle this automatically. After you run depmod -a, modprobe reads the dependency database and loads the provider before the consumer without you needing to think about the order:
# Install modules first
$ sudo cp provider_module.ko consumer_module.ko /lib/modules/$(uname -r)/extra/
# Update the dependency database
$ sudo depmod -a
# modprobe loads provider_module automatically first, then consumer_module
$ sudo modprobe consumer_module
Always prefer modprobe over insmod when your modules have dependencies. Reserve insmod for quick testing of standalone modules or when you are explicitly controlling load order during development.
Reading lsmod Output — The “Used By” Column
After loading both modules, run lsmod:
$ lsmod | grep -E "consumer|provider"
consumer_module 16384 0
provider_module 16384 1 consumer_module
| Column | Value in example | What it means |
|---|---|---|
| Module | provider_module |
The module name (without .ko) |
| Size | 16384 |
Memory used by the module in bytes |
| Used by (count) | 1 |
Reference count — how many things currently use this module |
| Used by (list) | consumer_module |
Names of modules that depend on this one |
The reference count for provider_module is 1 because consumer_module is currently using its exported symbols. The reference count for consumer_module is 0 because nothing depends on it — it sits at the top of the stack.
A reference count of 0 means the module can be safely removed. A count greater than 0 means something still depends on it, and removal will fail.
Removing Stacked Modules — Order Matters Again
Just like loading, removal has a required order. You must remove the consumer first, then the provider. If you try to remove the provider while the consumer is still loaded:
$ sudo rmmod provider_module
rmmod: ERROR: Module provider_module is in use by: consumer_module
The correct way with rmmod — remove consumer first:
$ sudo rmmod consumer_module
$ sudo rmmod provider_module
$ dmesg | tail
consumer_module: unloaded
provider_module: unloaded
The easy way with modprobe — removes the whole stack in correct order automatically:
$ sudo modprobe -r consumer_module
# This also removes provider_module if nothing else uses it
modprobe -r works top-down: it removes the named module, then checks if the modules it depended on are now unused, and removes those too. This is the recommended approach for any production use.
Continue to Part 2 of this lecture where we cover module reference counting internals, GPL license enforcement in depth, symbol namespaces in Linux 6.x, and the full dependency pipeline from build to runtime.
Key Takeaways — Part 1
Frequently Asked Questions
Q: Can a module export a variable, not just a function?
Yes. You can export any global variable the same way. Put EXPORT_SYMBOL(variable_name) after the variable definition. The consumer declares it with extern int variable_name; and reads or writes it directly. Be careful with concurrent access — if multiple modules read and write the same variable you need proper locking.
Q: What does “Unknown symbol in module” mean exactly?
It means the kernel tried to resolve a symbol name at module load time and could not find it in the kernel’s symbol table. Either the provider module is not loaded yet, or the provider did not export that symbol at all (missing EXPORT_SYMBOL), or the name is misspelled. Check dmesg — it will print the exact symbol name that failed.
Q: Do I need a shared header file between the two modules?
It is a good practice but not strictly required. For simple cases a plain extern declaration in the consumer’s source file is enough. For real projects, create a shared header like provider_api.h and include it in both modules. This avoids declaration mistakes and makes the API contract clear.
Q: What happens if I load the same module twice?
The kernel rejects it. You get an error like insmod: ERROR: could not insert module: File exists. The module is already in the kernel’s module list, and the kernel will not load a second copy. This is intentional — you cannot have two copies of the same module loaded simultaneously.
Q: How do I check what symbols a .ko file exports?
Use nm on the .ko file and look for uppercase symbol types:
$ nm provider_module.ko | grep " T "
0000000000000000 T ep_add
0000000000000050 T ep_show_platform_info
Or use modinfo which also shows the dependency information:
$ modinfo consumer_module.ko
filename: consumer_module.ko
license: GPL
depends: provider_module
vermagic: 6.x.x SMP mod_unload
Q: Can module stacking go more than two levels deep?
Yes. Module A can export symbols used by module B, which in turn exports symbols used by module C. This creates a three-level stack. The loading order must still go bottom-up (A then B then C) and removal must go top-down (C then B then A). modprobe handles this automatically when you install the modules and run depmod.
Q: Is module stacking used in real Linux drivers?
Absolutely. It is the standard design pattern for kernel subsystems. The USB stack, ALSA audio, the filesystems (msdos depends on fat), the network device stack, the DRM/GPU subsystem — all use module stacking extensively. When you write a driver for a USB device, your module automatically stacks on usbcore without you having to think much about it.
Q: Why does rmmod sometimes say “Module is in use” even though lsmod shows 0?
A module reference count above zero in lsmod is not the only reason rmmod can fail. Some drivers register themselves with a subsystem at load time (like a platform driver registering with the platform bus). Even if no other module lists them as a dependent, the reference count in the subsystem may keep them alive. In those cases check for running processes using the device node with lsof or fuser, or check if a device file is still open.
Authoritative References
Free Linux Kernel Programming Course
EmbeddedPathashala offers completely free courses on Linux kernel programming, Linux device drivers, and embedded systems — no signup required.
Visit EmbeddedPathashala