Linux Kernel Module Stacking and EXPORT_SYMBOL Explained

Linux Kernel Module Stacking and EXPORT_SYMBOL Explained
Master symbol exporting, module dependencies, and library-style kernel architecture — with real working code
⏱ ~25 min read
🎯 Beginner → Intermediate
🐧 Linux 6.x
📦 Free Course

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() and EXPORT_SYMBOL_GPL() do and why they matter
  • How linux kernel module stacking works conceptually and in practice
  • How to read lsmod output 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 extern declarations 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.

Figure 1 — How EXPORT_SYMBOL() Makes Symbols Visible Across Modules
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;
}
↓   symbol resolution at insmod time   ↓
KERNEL EXPORTED SYMBOL TABLE
Symbol Address License
llkd_sysinfo2 0xffff_c1234 GPL
llkd_data 0xffff_c1290 GPL

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.

Figure 2 — Linux Kernel Module Stacking: VirtualBox Example
vboxnetadp.ko
Dependent module
usage count: 0
vboxnetflt.ko
Dependent module
usage count: 1
↓   both depend on   ↓
vboxdrv.ko  — Core / Library Module
usage count: 3
EXPORT_SYMBOL(vbox_api_init);
EXPORT_SYMBOL(vbox_api_destroy);
EXPORT_SYMBOL(vbox_idc_connect);
↓   loaded into   ↓
🐧   Linux Kernel 6.x

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/GPL as 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 extern declarations 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 modprobe with a proper modules.dep dependency entry so the kernel handles ordering automatically.
  • Use modprobe over insmod. modprobe reads /lib/modules/$(uname -r)/modules.dep and automatically loads dependencies in the correct order. During development, insmod is fine, but production systems should use modprobe.
  • Test unload paths carefully. Always verify that your module’s exit function properly undoes everything the init function 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.ko core module exports hundreds of symbols consumed by vendor-specific GPU modules like i915.ko, amdgpu.ko, and nouveau.ko.
  • USB subsystem. usbcore.ko is the library module. Every USB device driver (HID, storage, networking) is stacked on top of it.
  • Bluetooth. bluetooth.ko exports the core HCI and L2CAP APIs. Protocol-specific modules like rfcomm.ko, bnep.ko, and hidp.ko are 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_SIG enabled (mandatory in many distros), all modules — including stacked ones — must be signed with a trusted key. An unsigned user_lkm.ko will be rejected even if core_lkm.ko is 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/modules or lsmod — any unexpected module appearing in the dependency chain is worth investigating.

Summary and Key Takeaways

  • EXPORT_SYMBOL() and EXPORT_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 lsmod third 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

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 →

Leave a Reply

Your email address will not be published. Required fields are marked *