EXPORT_SYMBOL() and Variable Scope in Linux Kernel Modules

EXPORT_SYMBOL in Linux Kernel Modules — Symbol Visibility & Variable Scope | EmbeddedPathashala

Free Linux Kernel Programming Course › LKM Series › Module 5

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.

Linux 6.x EXPORT_SYMBOL Module Stacking Symbol Table Free Course

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.

The golden rule of kernel module scoping in Linux 6.x: A symbol (variable or function) defined in a kernel module is private to that module unless you explicitly export it. Two different modules can have a global named dev_count and there is zero conflict — each module’s dev_count is completely separate.
Each Loaded Module Lives in Its Own Symbol Namespace

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.

How EXPORT_SYMBOL() Makes a Symbol Globally Available
Important Detail The order of loading matters. Module B cannot be loaded if it depends on a symbol exported by Module A and Module A has not been loaded yet. The kernel enforces this dependency at 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:

🔒 Private — Not Exported
/* 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;
}
🔓 Exported — Globally Visible
/* 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:

  1. The static keyword is removed when exporting. A static symbol is scoped to the translation unit at the C level, so it cannot be exported at the kernel level either.
  2. 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;
}
Linux 6.x Header Practice In production drivers, rather than writing 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.

How to Check What Is Exported On your running system, /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.

Module Stacking — Layered Driver Architecture

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.

EXPORT_SYMBOL()
/* Any module can use this
 * regardless of license */

int sensor_hw_read(int reg);
EXPORT_SYMBOL(sensor_hw_read);
EXPORT_SYMBOL_GPL()
/* 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.

Tainted Kernel Warning Loading a non-GPL module that uses only 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
Symbol Versioning in Linux 6.x The kernel uses MODVERSIONS to ensure that a module compiled against one version of an exported interface cannot be accidentally loaded against a different version (which might have a changed function signature). Each exported symbol gets a CRC hash computed from its type signature. If the hash doesn’t match at load time, the kernel rejects the module with a version mismatch error. This protects against subtle, hard-to-debug crashes caused by ABI mismatches.

9. Interview Questions

By default, is a global variable in a kernel module visible to other modules? Why or why not?
No. Since Linux 2.6, all symbols in a kernel module — including non-static globals — are private to that module by default. This prevents namespace collisions between unrelated modules and makes the kernel more robust. You must explicitly use EXPORT_SYMBOL() to make a symbol visible outside the module.
What is the difference between EXPORT_SYMBOL() and EXPORT_SYMBOL_GPL()?
Both make a symbol available in the global kernel symbol table. The difference is licensing: 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.
Can you export a static variable or function from a kernel module?
No. The 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.
What is module stacking and where do you see it in real Linux drivers?
Module stacking is a layered design where a higher-level module uses symbols exported by a lower-level module. Common examples include USB device drivers that call functions exported by the USB core module (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.
What happens if you try to load a module that depends on an unexported kernel symbol?
The kernel will refuse to load the module and print an error about an unresolved symbol. The module system checks all symbol references at load time before executing any module code. If any required symbol is not present in the global symbol table (either from the kernel core or from already-loaded modules), the load is aborted cleanly.
Two kernel modules each have a global variable named retry_count. Will they conflict?
No, there is no conflict. Since kernel 2.6, each module’s symbols are private to it by default. The two 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.
What is /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.
What is 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.

Part of the Free Linux Kernel Programming Course at EmbeddedPathashala

Free Linux device drivers course • Free embedded systems course • No registration required

Leave a Reply

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