EXPORT_SYMBOL() and Variable Scope in Linux Kernel Modules
How the kernel controls which symbols your module shares with the outside world — and why getting this right matters for building real device drivers.
1. A Quick Recap of Variable Scope in C
Before looking at the kernel-specific behaviour, it is worth reminding ourselves of how C handles variable and function visibility, because the kernel builds directly on these rules.
| Declaration style | Where it is visible | Does it retain its value between calls? |
|---|---|---|
| Local variable inside a function | Only inside that function | No — stack-allocated, gone when function returns |
static local variable inside a function |
Only inside that function | Yes — stored in BSS/data segment, persists across calls |
static global variable or function (file scope) |
Only within the same .c translation unit |
Yes — persists for the program’s lifetime |
| Non-static global variable or function | Any .c file in the same linked binary |
Yes — persists for the program’s lifetime |
In a normal userspace program, a non-static global is visible to every file that shares the same link step. In the kernel world, however, this changes significantly once you are dealing with loadable modules — and that is exactly what this lecture is about.
2. The Kernel Module Default: Everything Is Private
Here is something that surprises many developers coming from embedded C backgrounds: in a kernel module, every variable and every function — even non-static globals — is by default visible only within that module. Nothing leaks out to the rest of the kernel unless you explicitly ask it to.
This was not always the case. In the 2.4 kernel era, global symbols in a module were automatically visible across the entire running kernel. That created a lot of problems — symbol name clashes between unrelated modules, unexpected side effects, and debugging nightmares. The decision was reversed in the 2.6 series and has remained this way ever since. In Linux 6.x, private-by-default is firmly the standard.
dev_count and there is zero conflict — each module’s dev_count is completely separate.
|
Module A (lkm_a.ko) int dev_count = 0; static int irq_num = 3; |
‖ |
Module B (lkm_b.ko) int dev_count = 0; static int irq_num = 7; |
‖ |
Kernel Core Cannot see either module’s dev_count |
| Module A namespace No clash with B |
Module B namespace No clash with A |
Kernel namespace (exported symbols only) |
3. What EXPORT_SYMBOL() Actually Does
When you apply EXPORT_SYMBOL() to a variable or function, the kernel build system adds an entry for that symbol into a special section of the module’s ELF binary called __ksymtab. This section contains the symbol’s name as a string and a pointer to its address in memory.
When the module is loaded, the kernel’s module loader reads this __ksymtab section and adds all the exported symbols into the global kernel symbol table. From that point forward, any other module that declares an extern reference to that symbol will have it resolved correctly at load time.
Module source codeEXPORT_SYMBOL(my_func);
|
→ |
Kbuild adds entry to__ksymtab section in .ko
|
→ |
insmod loads module,kernel registers symbol |
→ | |
| ↓ | ||||||
| Symbol is now in the global kernel symbol table — any subsequently loaded module can use it | ||||||
insmod time. modprobe handles this automatically by reading modules.dep, which is generated by depmod.
4. How to Export a Symbol — Before and After
Let us see what the code looks like with and without exporting. Start with two symbols in a module called sensor_core:
/* Both are private to this module.
* Nothing outside can see them.
* The 'static' keyword enforces this
* at the C level too. */
static int sample_rate = 100;
static long read_sensor(int ch)
{
/* ... hardware access ... */
return 0;
}
/* Remove 'static'. Then export.
* Other modules and the kernel core
* can now see and use these. */
int sample_rate = 100;
EXPORT_SYMBOL(sample_rate);
long read_sensor(int ch)
{
/* ... hardware access ... */
return 0;
}
EXPORT_SYMBOL(read_sensor);
Notice two things:
- The
statickeyword is removed when exporting. Astaticsymbol is scoped to the translation unit at the C level, so it cannot be exported at the kernel level either. - The
EXPORT_SYMBOL()call goes after the function definition, not before. This is a common source of mistakes for newcomers.
Now, any other module that wants to call read_sensor() simply declares it as extern and calls it — exactly as you would in userspace with a shared library function:
/* In another module that depends on sensor_core */
/* Declare that this symbol exists in another module */
extern long read_sensor(int ch);
extern int sample_rate;
static int __init consumer_init(void)
{
int ch = 0;
long val;
pr_info("Current sample rate: %d\n", sample_rate);
val = read_sensor(ch);
pr_info("Sensor channel %d reads: %ld\n", ch, val);
return 0;
}
extern declarations directly in your .c file, put the exported symbol declarations in a shared header file (e.g., sensor_core.h) that any dependent module can include. This is the standard practice used throughout the kernel source tree and avoids declaration mismatches.
5. What the Kernel Itself Exports and Why
The kernel core exports a large set of its own functions and variables using EXPORT_SYMBOL(). This is what makes it possible to write kernel modules at all — without these exports, your module code would have no way to call kernel services.
Some everyday examples that you will encounter constantly when writing device drivers:
| Exported Symbol | What It Does | Defined In |
|---|---|---|
request_irq() |
Register an interrupt handler for a hardware IRQ line | kernel/irq/manage.c |
free_irq() |
Release a previously registered interrupt handler | kernel/irq/manage.c |
kmalloc() |
Allocate physically contiguous kernel memory | mm/slab_common.c |
kfree() |
Free memory allocated with kmalloc() |
mm/slab_common.c |
printk() / pr_info() |
Write a message to the kernel log buffer | kernel/printk/printk.c |
strscpy() |
Safe bounded string copy (preferred over strcpy) |
lib/string.c |
copy_to_user() |
Safely copy data from kernel space to userspace | lib/usercopy.c |
mutex_lock() / mutex_unlock() |
Acquire/release a sleeping mutual exclusion lock | kernel/locking/mutex.c |
On the flip side, many important kernel functions are deliberately not exported. Internal scheduler functions, core memory management routines, and architecture-specific assembly helpers remain private to the kernel core. If a module tries to call a non-exported function, the linker will refuse to link the module at build time with an unresolved symbol error.
/proc/kallsyms lists all kernel symbols and their types. Exported symbols have a type of T (uppercase, meaning globally visible text section). You can also look at the Module.symvers file generated when you build the kernel — it contains the complete list of exported symbols along with their CRC checksums.
6. Module Stacking — Building Drivers in Layers
Module stacking is the design pattern where one kernel module exports functions that another module at a higher abstraction level uses. You see this throughout the kernel — especially in subsystems like USB, I2C, SPI, and network device drivers.
A concrete example: the kernel’s USB core module exports functions like usb_register() and usb_submit_urb(). Your USB device driver module calls these functions. Your driver depends on the USB core being loaded first. This layering keeps each module focused on one level of the stack and makes the whole system easier to maintain and extend.
|
Your Device Driver Module my_usb_device.ko calls: usb_register(), usb_submit_urb(), i2c_add_adapter() |
| ↓ depends on ↓ |
|
Subsystem Module Layer usbcore.ko | i2c-core.ko | spi.ko exports: usb_register(), i2c_add_adapter(), spi_async() |
| ↓ depends on ↓ |
|
Kernel Core (vmlinux) Always present — cannot be unloaded exports: kmalloc(), request_irq(), printk(), copy_to_user(), … |
When you are building an I2C temperature sensor driver, for example, your module lives at the top layer. It calls i2c_add_adapter() and i2c_transfer() exported by the I2C core module, which in turn calls kmalloc() and request_irq() exported by the kernel core. Each layer only knows about the layer directly below it.
Managing Module Dependencies
When Module B depends on symbols exported by Module A, the kernel enforces that Module A is loaded first. If you try to load Module B alone, the kernel will reject it with an error about unresolved symbols. The modprobe tool reads the dependency information from /lib/modules/$(uname -r)/modules.dep (generated by depmod -a) and automatically loads dependencies in the correct order.
# View the dependency tree for a module
modinfo my_usb_device.ko
# Load with automatic dependency resolution
modprobe my_usb_device
# Check what modules are currently loaded and their use counts
lsmod | grep usb
# You cannot rmmod a module that others depend on
# rmmod usbcore <-- This will fail if my_usb_device is still loaded
7. EXPORT_SYMBOL_GPL() — the GPL-Only Variant
There is a second export macro, EXPORT_SYMBOL_GPL(), that works identically to EXPORT_SYMBOL() in terms of making a symbol visible — but with one important restriction: the kernel will only allow a module to use a _GPL-exported symbol if that module declares itself as GPL-licensed (or a compatible open-source license).
If a proprietary (closed-source) module tries to use a EXPORT_SYMBOL_GPL() symbol, the kernel will refuse to load it. The module’s MODULE_LICENSE() macro determines whether it qualifies.
/* Any module can use this
* regardless of license */
int sensor_hw_read(int reg);
EXPORT_SYMBOL(sensor_hw_read);
/* Only GPL-licensed modules
* can call this function */
int sensor_hw_read(int reg);
EXPORT_SYMBOL_GPL(sensor_hw_read);
A significant portion of the kernel’s own exported interfaces use EXPORT_SYMBOL_GPL() — particularly in newer subsystems. This is a deliberate design choice to encourage open-source driver development. When you write a driver for Linux, using MODULE_LICENSE("GPL v2") or MODULE_LICENSE("GPL") gives you access to the full set of kernel exports.
EXPORT_SYMBOL() symbols is technically allowed, but the kernel marks itself as “tainted.” You will see WARNING: loading out-of-tree module taints kernel in the kernel log. A tainted kernel makes it harder to report bugs upstream, because the kernel community cannot be sure whether the proprietary module caused a problem.
8. Where Exported Symbols Live — the Kernel Symbol Table
All exported symbols — from the kernel core and from all loaded modules — are collected into the kernel’s global symbol table at runtime. Understanding how to inspect this table is a practical skill you will use often during driver development and debugging.
# View all currently exported kernel symbols
# Format: address type name [module]
# Uppercase type letter = globally visible
# 'T' = exported text (function)
# 'D' = exported data (variable)
cat /proc/kallsyms | head -30
# Search for a specific exported function
cat /proc/kallsyms | grep " T request_irq"
# See symbols exported by a specific module
cat /proc/kallsyms | grep "\[sensor_core\]"
# List all exported symbols from a .ko file before loading it
nm mymodule.ko | grep " T " # exported functions
nm mymodule.ko | grep " D " # exported data
When you build an out-of-tree module that depends on another module’s exports, the build system needs to know the CRC checksums of those exported symbols to enforce version compatibility. This information comes from a Module.symvers file. If your dependent module fails to build with errors about missing symbols, it usually means the Module.symvers from the dependency module is not in your build path.
# When building Module B that depends on Module A's exports:
# First build Module A, which generates Module.symvers
# Then copy or reference that file when building Module B
make -C /lib/modules/$(uname -r)/build \
M=$(PWD) \
KBUILD_EXTRA_SYMBOLS=/path/to/moduleA/Module.symvers \
modules
9. Interview Questions
EXPORT_SYMBOL() to make a symbol visible outside the module.EXPORT_SYMBOL() and EXPORT_SYMBOL_GPL()?EXPORT_SYMBOL_GPL() symbols can only be used by modules that declare a GPL-compatible license via MODULE_LICENSE(). A proprietary module trying to use a _GPL-exported symbol will be refused by the kernel at load time.static variable or function from a kernel module?static keyword limits a symbol’s scope to the current translation unit at the C language level. You must remove static before calling EXPORT_SYMBOL() on it. Trying to export a static symbol will either fail at build time or produce undefined behaviour.usbcore.ko), I2C drivers that use the I2C core module, and network drivers that use the networking stack. Each layer is a separate .ko file that depends on the layer below it.retry_count. Will they conflict?retry_count variables are completely separate — they have different memory addresses and neither module can see the other’s variable unless one of them explicitly exports it./proc/kallsyms and how is it useful during kernel development?/proc/kallsyms is a virtual file that lists all symbols currently known to the running kernel — both from the kernel core and from all loaded modules. It shows each symbol’s memory address, type, and name. During development, it is useful for verifying that your module’s exported symbols are correctly registered after loading, and for looking up the addresses of functions when interpreting oops/panic stack traces.Module.symvers and why do you need it when building a module that depends on another module?Module.symvers is a file generated during a kernel module build that lists all exported symbols along with their CRC version hashes. When building Module B that depends on symbols from Module A, you need Module A’s Module.symvers so the build system can verify symbol compatibility and embed the correct CRC values in Module B. Without it, the kernel may refuse to load Module B due to version mismatch errors.