Free Linux Device Drivers Course — Module 5, Part B
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.
- 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, andlsmoddo - Basic understanding of shared libraries in user-space Linux (
.sofiles)
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.
| 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.
| 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 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
.kofiles and rundepmod -a, the kernel’s dependency tracking automatically loadsmylib_module.kobeforemydevice_a.kowhen you runmodprobe 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
| 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
| 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-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 -aafter installing modules: If you copy your.kofiles to/lib/modules/$(uname -r)/but forget to rundepmod -a, modprobe does not know about the dependency and will not auto-load the base module. Always rundepmod -aafter installing kernel modules. - Using insmod instead of modprobe with stacked modules:
insmoddoes not read dependency information. If module B depends on module A and you tryinsmod mydevice_b.kowithout loading mylib_module first, you get an “Unknown symbol in module” error. Usemodprobefor 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 plainEXPORT_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 ownMODULE_LICENSE(). It is a separate.kofile and is treated independently by the kernel.
Troubleshooting — Common Error Messages and Their Fixes
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: 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.
# 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 plainEXPORT_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
insmodandmodprobepaths for stacked modules.modprobewith proper installation is what users will actually use; verify it works end-to-end.
- Libraries as user-space programs know them (shared
.sofiles, dynamic linker) do not exist in kernel space. - Technique 1 — Multi-source single module: Multiple
.cfiles listed inmodulename-objsin 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()orEXPORT_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 -ahandle 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
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.
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.
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.
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.
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.
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.rstin 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