Library Emulation in Linux Kernel Modules

Library Emulation in Linux Kernel Modules: Multi-Source Files and Module Stacking | Free Linux Device Drivers Course
Library Emulation in Linux Kernel Modules

Free Linux Device Drivers Course — Module 5, Part B

⏱ 30 min read
🎯 Intermediate
🐧 Linux 6.x
🔧 Architecture Focus

One of the biggest mindset shifts when moving from user-space C programming to Linux kernel module development is realizing that the entire concept of a shared library simply does not exist in kernel space. There is no libc in the kernel. There is no dlopen(). You cannot link against a .so file from a kernel module. Everything you call must either be in the kernel’s own code or in another module that is already loaded.

This free Linux device drivers course tutorial explains why libraries do not exist in the kernel, then walks through the two main techniques used in real-world kernel and driver development to achieve “library-like” code reuse: multiple source files linked into a single module, and module stacking. By the end, you will know which technique to use and when.

📚 What You Will Learn
Why libraries do not exist in kernel space How user-space and kernel-space code reuse differ Technique 1: Multiple .c files → single .ko module Makefile syntax for multi-source kernel modules Technique 2: Module stacking with EXPORT_SYMBOL EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL Module dependency management with modprobe When to use each technique — trade-offs
✅ Prerequisites
  • You should have completed the previous lecture on static analysis and licensing
  • Comfortable writing a single-file kernel module that compiles cleanly
  • Understand what insmod, rmmod, modprobe, and lsmod do
  • Basic understanding of shared libraries in user-space Linux (.so files)

Why Libraries Do Not Exist in Linux Kernel Space

In user-space Linux programming, libraries are everywhere. libc provides standard C functions. libpthread provides POSIX threads. libm gives you math functions. These shared libraries live on disk as .so files, and the dynamic linker loads them into a process’s virtual address space at runtime. Multiple processes can share the same physical memory pages that hold the library code.

The kernel has a completely different architecture. There is no dynamic linker in the kernel. There are no shared virtual memory mappings between independently-compiled code units. Everything that runs inside the kernel shares a single, flat, non-paged address space. The concept of “load this library into the kernel” does not map onto how the kernel is structured.

Code Reuse: User Space vs Kernel Space — Fundamental Architecture Difference
Aspect User Space Kernel Space
Library concept Shared libraries (.so files loaded by dynamic linker) Does not exist — no dynamic linker in the kernel
Code reuse across binaries dlopen(), dlsym(), shared symbol tables EXPORT_SYMBOL() + module stacking
Multiple source files → one binary Compiler + linker (gcc -o program a.o b.o c.o) Kernel Makefile: modulename-objs := a.o b.o c.o
Address space Separate virtual address space per process Single shared kernel virtual address space
Symbol visibility Controlled by visibility attributes and linker scripts Controlled by EXPORT_SYMBOL() and EXPORT_SYMBOL_GPL()

So what do kernel and driver developers actually do when they need shared utility functions across multiple modules? They have two main options, and we will cover both in detail.

Technique 1 — Multiple Source Files Linked Into a Single Kernel Module

The first technique is simpler and often preferable: put all your code into multiple .c files, and tell the kernel build system to compile and link them all together into a single .ko binary. From the user’s perspective (and from the kernel’s perspective), it is still one module with one name. Internally, it was compiled from several source files.

This is exactly how most real Linux kernel drivers are structured. The network stack’s drivers, the SCSI subsystem, GPU drivers — almost all of them span multiple source files compiled into a single module or built-in object. It is the standard approach.

How the Kernel Build System Handles Multiple Source Files

The kernel’s Kbuild system has a specific Makefile syntax for this. You use a variable named modulename-objs (where modulename is the name of your .ko file) to list all the object files that should be linked together.

# Makefile for a kernel module built from multiple source files
# The resulting module will be called: mydriver.ko

obj-m := mydriver.o

# List all the .c files that make up this single module
# Kbuild compiles each one to a .o, then links them into mydriver.ko
mydriver-objs := mydriver_main.o \
                 mydriver_utils.o \
                 mydriver_sysfs.o

# Standard build invocation for Linux 6.x
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

With this Makefile, when you run make, Kbuild compiles mydriver_main.c, mydriver_utils.c, and mydriver_sysfs.c separately into .o files, then links them all together into mydriver.ko. The resulting module has one name, one module_init(), one module_exit(), and one MODULE_LICENSE().

How the Source Files Refer to Each Other

Since all the source files become part of the same module, they share symbols freely — no special export mechanism is needed. You simply use a shared header file to declare the functions and data structures that need to be visible across the files.

/* mydriver_internal.h — shared header used by all source files
 * of the mydriver kernel module
 * SPDX-License-Identifier: GPL-2.0-only
 */
#ifndef MYDRIVER_INTERNAL_H
#define MYDRIVER_INTERNAL_H

#include <linux/types.h>

/* Declared in mydriver_utils.c, used by mydriver_main.c */
int mydriver_validate_config(unsigned int flags);
void mydriver_dump_state(void);

/* Declared in mydriver_sysfs.c, used by mydriver_main.c */
int mydriver_sysfs_init(struct device *dev);
void mydriver_sysfs_remove(struct device *dev);

#endif /* MYDRIVER_INTERNAL_H */

Each .c file that needs these functions simply includes mydriver_internal.h. The linker resolves all references when building the final .ko. This is identical to how you would structure a multi-file user-space C program.

Multi-Source Kernel Module Build Process (Linux 6.x Kbuild)
mydriver_main.c
GCC
mydriver_main.o
ld
(link)
mydriver.ko

Single module
One insmod
One license
mydriver_utils.c mydriver_utils.o
mydriver_sysfs.c mydriver_sysfs.o
All .o files linked by Kbuild into one .ko. No EXPORT_SYMBOL needed between them — they share symbols internally.

Advantages of the Multiple-Source-File Approach

  • Simplicity: One module, one insmod, no dependency ordering to worry about.
  • No symbol export needed: All internal functions are visible across files without any EXPORT_SYMBOL() calls.
  • Easier debugging: One kernel module means one set of dmesg messages and one module name in lsmod.
  • Performance: No inter-module function call overhead. All calls are direct.
  • Better encapsulation: Internal implementation details stay hidden — you control exactly what is exported to the wider kernel via header files.

When the Multiple-Source Approach Is the Right Choice

Use this technique when you are building a self-contained driver or subsystem where all the “library” code is specific to that module. If the utility functions you are writing would only ever be useful to this one driver, there is no reason to make them available to other modules. Keep everything in one .ko.

Technique 2 — Module Stacking: Sharing Code Across Multiple Kernel Modules

Sometimes the utility functions you write are genuinely useful to more than one kernel module. Perhaps you are building a hardware family with several device variants, all sharing a common bus communication layer. Or you are writing a protocol handler that multiple transport-layer modules will call into. In these cases, duplicating the code into each module is wasteful and creates a maintenance nightmare. This is where module stacking comes in.

Module stacking means one kernel module exports some of its functions using EXPORT_SYMBOL(), and another kernel module imports and calls those functions. The exporting module is a dependency — it must be loaded first. The importing module sits “on top” of it in the stack.

The EXPORT_SYMBOL() Mechanism

By default, a function defined in a kernel module is visible only to that module. If you want other modules to call it, you must explicitly export it. The kernel provides two macros for this:

/* In the "library" module — mylib_module.c
 * SPDX-License-Identifier: GPL-2.0-only
 */
#include <linux/module.h>
#include <linux/kernel.h>

/* This function can be called by any module, GPL or non-GPL */
int mylib_init_hardware(unsigned int device_id)
{
    pr_info("mylib: initializing device 0x%x\n", device_id);
    /* ... real hardware init code ... */
    return 0;
}
EXPORT_SYMBOL(mylib_init_hardware);

/* This function can ONLY be called by GPL-licensed modules */
int mylib_read_secure_register(unsigned int reg)
{
    /* access to sensitive hardware register */
    return 0;
}
EXPORT_SYMBOL_GPL(mylib_read_secure_register);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Shared library module for mylib hardware family");

The difference between the two macros matters:

EXPORT_SYMBOL() vs EXPORT_SYMBOL_GPL() — Who Can Call What
Export Macro Callable by GPL module? Callable by Proprietary module? Typical Use Case
EXPORT_SYMBOL(fn) Yes ✓ Yes ✓ General utilities, non-sensitive APIs
EXPORT_SYMBOL_GPL(fn) Yes ✓ No — linker error ✗ Security-sensitive APIs, kernel-internal interfaces, anything the community wants GPL-only

The Importing Module — Calling Exported Functions

On the other side, the module that wants to use exported functions from another module declares them as external references using a shared header file. The kernel’s module loader resolves these at insmod time.

/* mylib_module.h — public API header for the mylib shared module
 * Distributed with mylib_module.ko, included by users of it
 * SPDX-License-Identifier: GPL-2.0-only
 */
#ifndef MYLIB_MODULE_H
#define MYLIB_MODULE_H

/* Exported with EXPORT_SYMBOL — available to all modules */
int mylib_init_hardware(unsigned int device_id);

/* Exported with EXPORT_SYMBOL_GPL — only GPL modules may call this */
int mylib_read_secure_register(unsigned int reg);

#endif
/* mydevice_a.c — a driver that uses the shared mylib_module
 * SPDX-License-Identifier: GPL-2.0-only
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include "mylib_module.h"   /* include the exported API header */

static int __init mydevice_a_init(void)
{
    int ret;

    pr_info("mydevice_a: loading\n");

    /* Call the function from the stacked module */
    ret = mylib_init_hardware(0xA001);
    if (ret) {
        pr_err("mydevice_a: hardware init failed: %d\n", ret);
        return ret;
    }

    return 0;
}

static void __exit mydevice_a_exit(void)
{
    pr_info("mydevice_a: unloading\n");
}

module_init(mydevice_a_init);
module_exit(mydevice_a_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Device A driver using shared mylib module");

/* Declare the dependency — modprobe uses this to load mylib_module first */
MODULE_SOFTDEP("pre: mylib_module");

Understanding MODULE_SOFTDEP and Dependency Loading

When module B depends on module A, A must already be loaded before B is inserted into the kernel. There are two ways this can work:

  • Hard dependency (automatic with modprobe): When you properly install both .ko files and run depmod -a, the kernel’s dependency tracking automatically loads mylib_module.ko before mydevice_a.ko when you run modprobe mydevice_a.
  • MODULE_SOFTDEP: An explicit soft dependency hint that tells modprobe “load this module before me.” This is useful when the dependency is not automatically detected through symbol resolution.
# With modules properly installed, modprobe handles dependency ordering:
sudo depmod -a
sudo modprobe mydevice_a      # automatically loads mylib_module first

# Check what got loaded and in what order:
lsmod | grep my
# Output:
# mydevice_a    12345  0
# mylib_module  23456  1 mydevice_a     ← "1 mydevice_a" means one module depends on it

# To unload, remove dependents first:
sudo modprobe -r mydevice_a   # removes mydevice_a first, then mylib_module
Module Stacking Architecture — Dependency Chain with EXPORT_SYMBOL
mydevice_a.ko
Consumer module A
mydevice_b.ko
Consumer module B
↓   calls EXPORT_SYMBOL functions   ↓
mylib_module.ko
Provider module — exports shared API
EXPORT_SYMBOL(mylib_init_hardware)
EXPORT_SYMBOL_GPL(mylib_read_secure_register)
↓ calls into
Linux Kernel 6.x (built-in core)
mylib_module must be loaded FIRST. Consumer modules can be loaded/unloaded independently as long as their dependency is satisfied. depmod + modprobe handle the ordering automatically when modules are installed properly.

Comparing Both Techniques — When to Use Each

Multi-Source File Linking vs Module Stacking — Decision Guide for Free Linux Device Drivers Course
Factor Multi-Source → Single .ko Module Stacking
Number of resulting .ko files One Two or more
Load/unload complexity Simple — one insmod Must respect dependency order; modprobe handles it
Code shared with other modules? No — internal only Yes — core use case
Symbol export needed? No Yes — EXPORT_SYMBOL() on every shared function
Best for Single device driver with multiple source files for organization Hardware family drivers, subsystem core + plugin architecture
Real-world example in Linux Most single-device drivers (e.g., a single Ethernet controller driver) USB core (usbcore.ko) + USB host controllers + USB device drivers
Performance overhead None — direct function calls within the module Minimal — inter-module calls still go through direct symbol resolution, not any indirection layer
Debugging complexity Low — one module in lsmod Moderate — multiple modules in lsmod; must trace calls across module boundaries

The general rule of thumb in Linux kernel and device driver development is: prefer the multi-source single-module approach unless you have a genuine reason to share the code across multiple separately-loaded modules. The extra complexity of module stacking (dependency ordering, EXPORT_SYMBOL discipline, depmod management) is only worth it when code reuse across multiple independent modules is a real requirement, not a speculative future one.

Real-World Pattern — How USB Drivers Use Module Stacking

The USB subsystem in Linux is one of the best examples of module stacking done right. Understanding it will make the pattern click.

USB Subsystem — Classic Module Stacking in Real Linux Kernel
usb-storage.ko
USB mass storage driver
usbhid.ko
USB HID (keyboard/mouse)
cdc_ether.ko
USB Ethernet adapter
↓ all depend on ↓
usbcore.ko
The shared USB “library” module — EXPORT_SYMBOL for all USB device operations
↓ depends on ↓
xhci-hcd.ko / ehci-hcd.ko — USB host controller drivers (hardware-level)

usbcore.ko is the “library” module. It exports hundreds of functions — usb_register(), usb_deregister(), usb_submit_urb(), and many more. Every USB device driver in the system calls into these functions. Without usbcore loaded, no USB device driver can function. This is module stacking at scale, and it is the correct architecture for this use case because the core USB protocol handling is genuinely shared across hundreds of different device drivers.

Common Mistakes with Multi-Source Modules and Module Stacking

  • Forgetting to run depmod -a after installing modules: If you copy your .ko files to /lib/modules/$(uname -r)/ but forget to run depmod -a, modprobe does not know about the dependency and will not auto-load the base module. Always run depmod -a after installing kernel modules.
  • Using insmod instead of modprobe with stacked modules: insmod does not read dependency information. If module B depends on module A and you try insmod mydevice_b.ko without loading mylib_module first, you get an “Unknown symbol in module” error. Use modprobe for stacked modules.
  • Exporting everything with EXPORT_SYMBOL when EXPORT_SYMBOL_GPL is more appropriate: If a function gives access to kernel internals that should be GPL-only, use EXPORT_SYMBOL_GPL(). Using plain EXPORT_SYMBOL() for everything weakens the GPL enforcement that the kernel community depends on.
  • Circular module dependencies: Module A cannot depend on module B while B also depends on A. The kernel module loader cannot resolve circular dependencies. If your design leads to circular dependencies, the architecture needs rethinking — split the shared code into a third, independent module that both A and B can depend on.
  • Forgetting to declare MODULE_LICENSE() in the shared library module: Every kernel module, including your “library” module, needs its own MODULE_LICENSE(). It is a separate .ko file and is treated independently by the kernel.

Troubleshooting — Common Error Messages and Their Fixes

Error: “Unknown symbol in module, or unknown parameter”
ERROR: could not insert module mydevice_a.ko: Unknown symbol in module

Cause: The shared library module (mylib_module.ko) is not loaded yet, or the symbol was not exported with EXPORT_SYMBOL().

Fix: Load the base module first (insmod mylib_module.ko), or use modprobe with proper depmod setup. If the symbol truly is exported, check that the base module’s .ko was compiled from the same kernel source tree.

Error: “GPL-incompatible module uses GPL-only symbol”
ERROR: modpost: GPL-incompatible module mydevice_a.ko uses GPL-only symbol 'mylib_read_secure_register'

Cause: Your module has a non-GPL license but is trying to call a function exported with EXPORT_SYMBOL_GPL().

Fix: Either change your module’s license to "GPL v2", or ask the library module author to export the function with plain EXPORT_SYMBOL() if they agree that is appropriate.

Error: “mydriver-objs” not recognized — module builds wrong
# Wrong: if your module name has a dash, the variable must match exactly
obj-m := my-driver.o
my-driver-objs := main.o utils.o   # This should work

# But be careful: Kbuild converts hyphens to underscores internally
# The safer approach: use underscores in module names
obj-m := my_driver.o
my_driver-objs := main.o utils.o

Best practice: Use underscores rather than hyphens in module names to avoid confusion between how the filename appears (my-driver.ko) and the Makefile variable (my_driver-objs).

Best Practices for Library Emulation in Linux Kernel Module Development

  • Start with a single module. Do not prematurely split your driver into a library + consumer architecture. Build it as one module first. Refactor to stacking only if you identify a concrete need to share code with another module.
  • Keep the shared header with the library module source. When you export symbols, provide a clean header file that exactly matches what is exported. Other driver authors depend on this header — treat it like a public API.
  • Use EXPORT_SYMBOL_GPL() by default for new exports. Only fall back to plain EXPORT_SYMBOL() if there is a specific reason a non-GPL module legitimately needs access to the function.
  • Document the load order explicitly. Add a comment at the top of every consumer module source file stating which other modules must be loaded before it. Even though depmod handles this automatically, the comment helps developers reading the code.
  • Always test both insmod and modprobe paths for stacked modules. modprobe with proper installation is what users will actually use; verify it works end-to-end.
📝 Key Takeaways
  • Libraries as user-space programs know them (shared .so files, dynamic linker) do not exist in kernel space.
  • Technique 1 — Multi-source single module: Multiple .c files listed in modulename-objs in your Makefile, compiled and linked into one .ko. Best for most drivers. No EXPORT_SYMBOL needed internally.
  • Technique 2 — Module stacking: One module exports functions with EXPORT_SYMBOL() or EXPORT_SYMBOL_GPL(); another module calls them. Required when the same code needs to be shared across multiple separately-loaded modules.
  • EXPORT_SYMBOL_GPL() restricts function access to GPL-licensed modules. Use it for sensitive or kernel-internal APIs.
  • modprobe + depmod -a handle module dependency ordering automatically when modules are properly installed.
  • Prefer the single-module multi-source approach unless code sharing across independent modules is a confirmed requirement.

Frequently Asked Questions — Free Linux Device Drivers Course

Q: Can a kernel module export C++ functions or use C++ features?

No. Kernel modules are written in C (and sometimes Rust in newer kernel versions). C++ is not supported in the kernel build system. The kernel does not use the C++ standard library, exceptions, RTTI, or any C++ runtime features. If you have C++ code you want to reuse in a kernel module, it must be rewritten in C.

Q: How does modprobe know which module to load first?

After you run depmod -a, it scans all .ko files in the current kernel’s module directory and builds a dependency database at /lib/modules/$(uname -r)/modules.dep. This database maps each module to the modules it depends on (determined by which symbols it imports). When you modprobe mydevice_a, modprobe reads this database, loads all dependencies first, then loads the requested module.

Q: What is the difference between insmod and modprobe?

insmod loads a single .ko file directly — you give it a file path and it loads that one module, ignoring any dependency tracking. If a required symbol is not already in the kernel, insmod fails with “Unknown symbol.” modprobe uses the module name (not the file path), reads the dependency database, loads dependencies first, and is the correct tool for production use. Use insmod only during development to load individual modules when you know all dependencies are already satisfied.

Q: My multi-source module Makefile is not working. What should I check first?

Three most common causes: (1) the modulename-objs variable name does not exactly match the obj-m target name without the .o extension; (2) you forgot a backslash continuation on one of the object file lines; (3) there are tabs vs spaces in the Makefile (Makefile rules must use a real tab character, not spaces). Run make V=1 to see the exact commands Kbuild is running — this makes it easy to spot what is missing.

Q: Can I have module A export a function and also call functions from module B at the same time?

Yes — a module can both export symbols and import symbols from another module. This creates a chain: C depends on B, B depends on A. What you cannot have is A depending on B while B also depends on A (circular dependency). Modprobe resolves chains but not cycles.

Q: Is module stacking used in production Linux device drivers?

Yes, extensively. The USB subsystem (usbcore + controller drivers + device drivers), the I2C subsystem (i2c-core + bus adapter drivers + device chip drivers), the SPI subsystem, the regulator framework, and many others all use module stacking. It is a well-established and tested pattern in the Linux kernel. The key is using it only where code sharing is genuinely needed, not for speculative future flexibility.

Authoritative References

  • Linux Kernel Documentation — Building External Modules: Documentation/kbuild/modules.rst in your kernel source — covers Kbuild multi-source module syntax
  • Linux Kernel Documentation — Exporting Symbols: Documentation/core-api/symbol-namespaces.rst — covers EXPORT_SYMBOL and symbol namespaces in Linux 6.x
  • Kernel Newbies Module Guide: https://kernelnewbies.org/FAQ/LKM — good introduction to LKM concepts
  • Linux USB Project: Study drivers/usb/core/ in the kernel source for a mature example of module stacking at scale

Continue Your Free Linux Device Drivers Journey

EmbeddedPathashala offers 100% free courses on Linux kernel development, Linux device drivers, embedded systems, and Bluetooth/BLE — no registration required.

Visit EmbeddedPathashala

Leave a Reply

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