Advanced Linux Kernel Modules: Parameters, Stacking & Symbol Exports

Linux Kernel Module Parameters, Stacking and Library Features | Free Linux Kernel Programming

EmbeddedPathashalaFree Linux Kernel Programming Course › Module Parameters, Stacking & Library Features

Linux Kernel Module Parameters, Stacking, and Library-like Features

Free Linux Kernel Programming Course — LKMs Part 2, Section 2

Level
Intermediate
Read Time
~22 min
Kernel
Linux 6.x
Cost
Free

Part of the Free Linux Kernel Programming Course at EmbeddedPathashala — updated for Linux 6.x

Topics Covered:
module_param macro Linux Module Stacking EXPORT_SYMBOL Kernel Library Modules Floating Point Kernel Free Linux Kernel Course module_param_array Free Device Drivers Course

What You Will Learn

  • How to pass parameters to a Linux kernel module at load time using module_param
  • How to pass arrays of values to a module using module_param_array
  • How to expose module parameters to sysfs so they can be changed at runtime
  • What module stacking is, why it exists, and how to implement it using EXPORT_SYMBOL
  • How to emulate library-like shared code between multiple kernel modules
  • Why floating point arithmetic is not allowed in the kernel and what to use instead
  • How to gather system information from within a kernel module

Passing Parameters to a Linux Kernel Module

In userspace, you pass arguments to a program on the command line. In the kernel, the equivalent mechanism for Linux kernel modules is module parameters. You can pass values to a module when you load it with insmod or modprobe, and you can even change some values at runtime through the /sys/module/ filesystem.

Module parameters are one of the most practical features you will use as a Linux kernel module developer. They let you make your module configurable without recompiling — you can change debug levels, buffer sizes, GPIO pin numbers, timeout values, and more.

The module_param Macro

The primary macro for declaring a module parameter is module_param(). It takes three arguments: the variable name, the type, and the permissions for the sysfs entry.

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Module Parameters Demo - Free Linux Kernel Course");

/* Declare parameters with default values */
static int debug_level = 0;
static char *device_name = "ep_device";
static int timeout_ms = 1000;

/* Register the parameters */
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Set debug verbosity level (0=off, 1=basic, 2=verbose)");

module_param(device_name, charp, 0444);
MODULE_PARM_DESC(device_name, "Name of the device to manage");

module_param(timeout_ms, int, 0644);
MODULE_PARM_DESC(timeout_ms, "Operation timeout in milliseconds (default: 1000)");

static int __init param_demo_init(void)
{
	pr_info("param_demo: loaded\n");
	pr_info("  debug_level  = %d\n", debug_level);
	pr_info("  device_name  = %s\n", device_name);
	pr_info("  timeout_ms   = %d\n", timeout_ms);
	return 0;
}

static void __exit param_demo_exit(void)
{
	pr_info("param_demo: unloaded\n");
}

module_init(param_demo_init);
module_exit(param_demo_exit);

Loading a Module with Parameters

# Load the module with custom parameter values
sudo insmod param_demo.ko debug_level=2 device_name="my_sensor" timeout_ms=500

# Check the kernel log
sudo dmesg | tail -10
# Output:
# param_demo: loaded
#   debug_level  = 2
#   device_name  = my_sensor
#   timeout_ms   = 500

# View the parameters in sysfs (they appear here after loading)
ls /sys/module/param_demo/parameters/
# debug_level  timeout_ms
# (device_name is not shown here because its permission is 0444 = read-only
#  from user POV but 0444 means it IS readable — check carefully)

# Read the current value of a parameter
cat /sys/module/param_demo/parameters/debug_level
# 2

# Change a parameter at runtime (only if sysfs permission allows write)
echo 1 > /sys/module/param_demo/parameters/debug_level
cat /sys/module/param_demo/parameters/debug_level
# 1
module_param — Permission Bits for sysfs Entry
Permission Value Meaning Typical Use
0 No sysfs entry created Parameter set only at load time, not visible after
0444 sysfs entry readable by all, not writable Read-only info: device name, hardware IDs
0644 Root can read/write, others can read Tunable at runtime: debug level, timeouts
0600 Root only — read and write Security-sensitive parameters
Warning: Never use 0666 (world-writable) for module parameters. This allows any unprivileged user to change kernel behavior. For tunable parameters, use 0644 at most — only root should be able to change kernel module settings.

Supported Parameter Types

Type String C Type Notes
bool bool Accepts 0/1, y/n, true/false
int int Signed 32-bit integer
uint unsigned int Unsigned 32-bit integer
long long Signed long
ulong unsigned long Unsigned long
charp char * String pointer — kernel allocates memory for it
short short Signed 16-bit
ushort unsigned short Unsigned 16-bit

Passing Arrays as Module Parameters

Sometimes you need to pass multiple values for a single parameter — like a list of GPIO pin numbers or IRQ numbers. The module_param_array macro handles this.

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");

#define MAX_PINS  8

static int gpio_pins[MAX_PINS] = {4, 17, 27, 22, 0, 0, 0, 0};
static int num_pins = 0;  /* will be filled by the kernel */

/* Declare array parameter */
module_param_array(gpio_pins, int, &num_pins, 0644);
MODULE_PARM_DESC(gpio_pins, "List of GPIO pin numbers (up to 8)");

static int __init array_param_init(void)
{
	int i;

	pr_info("array_param: %d pin(s) specified\n", num_pins);
	for (i = 0; i < num_pins; i++)
		pr_info("  gpio_pins[%d] = %d\n", i, gpio_pins[i]);

	return 0;
}

static void __exit array_param_exit(void)
{
	pr_info("array_param: unloaded\n");
}

module_init(array_param_init);
module_exit(array_param_exit);
# Pass multiple GPIO pins at load time (comma-separated)
sudo insmod array_param.ko gpio_pins=4,17,27,22

# Check output
sudo dmesg | tail -10
# array_param: 4 pin(s) specified
#   gpio_pins[0] = 4
#   gpio_pins[1] = 17
#   gpio_pins[2] = 27
#   gpio_pins[3] = 22
Tip: The third argument to module_param_array is a pointer to an integer that the kernel fills with the actual number of values passed. Always use this count variable instead of assuming all array slots were filled — users may pass fewer values than the maximum.

Understanding Linux Kernel Module Stacking

Module stacking is the mechanism that allows one Linux kernel module to use functions and data exported by another kernel module. It is how the kernel achieves modularity at the driver level — think of it as building a dependency chain between modules.

The most common real-world example of module stacking is the USB subsystem: usbcore exports functions that all USB device drivers (like usb-storage or cdc_acm) depend on. Another example is the I2C subsystem — i2c-core exports functions used by every I2C device driver.

Linux Kernel Module Stacking — Dependency Example
Kernel Module Dependency Stack
my_i2c_sensor.ko
(your device driver)
calls: i2c_transfer(), i2c_add_adapter()
depends on ↓
i2c-core.ko
(I2C subsystem)
exports: i2c_transfer(), i2c_add_adapter()
i2c-core.ko must be loaded BEFORE my_i2c_sensor.ko  |  modprobe handles this automatically via modules.dep

How to Export Symbols from a Kernel Module

To make a function or variable in your module available for other modules to use, you export it with EXPORT_SYMBOL() or EXPORT_SYMBOL_GPL(). The GPL version restricts use to GPL-licensed modules only.

/* === helper_module.c — the "library" module === */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Helper module exporting shared functions - Free Linux Kernel Course");

/* A shared function — available to other modules */
int ep_add_numbers(int a, int b)
{
	pr_info("helper_module: ep_add_numbers(%d, %d) called\n", a, b);
	return a + b;
}
EXPORT_SYMBOL_GPL(ep_add_numbers);

/* A shared variable */
int ep_shared_counter = 0;
EXPORT_SYMBOL_GPL(ep_shared_counter);

static int __init helper_init(void)
{
	pr_info("helper_module: loaded and symbols exported\n");
	return 0;
}

static void __exit helper_exit(void)
{
	pr_info("helper_module: unloaded\n");
}

module_init(helper_init);
module_exit(helper_exit);
/* === consumer_module.c — the module that depends on helper_module === */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Consumer module using helper_module - Free Linux Kernel Course");

/* Declare the external symbols we will use */
extern int ep_add_numbers(int a, int b);
extern int ep_shared_counter;

static int __init consumer_init(void)
{
	int result;

	pr_info("consumer_module: loaded\n");

	result = ep_add_numbers(10, 32);
	pr_info("consumer_module: ep_add_numbers(10, 32) = %d\n", result);

	ep_shared_counter++;
	pr_info("consumer_module: ep_shared_counter = %d\n", ep_shared_counter);

	return 0;
}

static void __exit consumer_exit(void)
{
	pr_info("consumer_module: unloaded\n");
}

module_init(consumer_init);
module_exit(consumer_exit);

Loading Stacked Modules in the Right Order

# Step 1: Load the helper module first (it exports the symbols)
sudo insmod helper_module.ko

# Step 2: Load the consumer module (it uses the exported symbols)
sudo insmod consumer_module.ko

# Check the output
sudo dmesg | tail -10
# helper_module: loaded and symbols exported
# consumer_module: loaded
# helper_module: ep_add_numbers(10, 32) called
# consumer_module: ep_add_numbers(10, 32) = 42
# consumer_module: ep_shared_counter = 1

# To unload, reverse the order — consumer first, then helper
sudo rmmod consumer_module
sudo rmmod helper_module

# What happens if you try to rmmod helper_module while consumer is loaded?
sudo rmmod helper_module
# rmmod: ERROR: Module helper_module is in use by: consumer_module
Note: The kernel’s module reference count prevents you from unloading a module that has dependent modules still loaded. This is a safety mechanism — it would be dangerous to unload a module whose exported functions are still being called by another module. Always unload in reverse dependency order.

EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL

Macro Who Can Use It When to Use
EXPORT_SYMBOL() Any module, including proprietary ones For general utility symbols that should be universally accessible
EXPORT_SYMBOL_GPL() Only GPL-licensed modules For core kernel subsystem functions — most in-tree kernel exports use this
Tip: When in doubt, use EXPORT_SYMBOL_GPL(). If you are writing open-source Linux kernel code — which is the recommended practice — all your modules should have MODULE_LICENSE("GPL") and you can use all GPL-exported symbols freely.

Checking Module Dependencies

# After make install and depmod -a, check dependencies
modinfo consumer_module | grep depends
# depends:  helper_module

# See the full dependency tree
modprobe --show-depends consumer_module
# insmod /lib/modules/6.8.0/extra/helper_module.ko
# insmod /lib/modules/6.8.0/extra/consumer_module.ko

# modprobe loads both automatically in the right order
sudo modprobe consumer_module

Emulating Library-like Features for Kernel Modules

In userspace, you share code between programs using shared libraries (.so files). In the kernel, there are no shared libraries in that sense. However, you can achieve a similar effect using the module stacking mechanism shown above — create a “library module” that exports commonly used functions, and have multiple other modules depend on it.

This pattern is widely used in the Linux kernel itself. The best examples are:

  • libata — a library module for SATA/PATA drivers. All SATA host controller drivers use it.
  • i2c-core — provides the core I2C framework used by all I2C device drivers.
  • spi-core — provides the SPI bus framework.
  • regmap — a generic register access library used by audio codecs, PMICs, and other register-based chips.

Building a Practical Library Module

Here is a more realistic example — a logging/tracing helper module that multiple driver modules in a product can share:

/* === ep_logger.c — shared logging helper module === */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>    /* for kmalloc */
#include <linux/string.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Shared logging helper - EmbeddedPathashala Free Kernel Course");

/* Configurable log prefix */
static char log_prefix[32] = "[EP]";
module_param_string(log_prefix, log_prefix, sizeof(log_prefix), 0644);
MODULE_PARM_DESC(log_prefix, "Prefix string for all log messages");

/*
 * ep_log_info - shared logging function
 * Called by other modules via EXPORT_SYMBOL_GPL
 */
void ep_log_info(const char *module_name, const char *msg)
{
	pr_info("%s [%s]: %s\n", log_prefix, module_name, msg);
}
EXPORT_SYMBOL_GPL(ep_log_info);

void ep_log_warn(const char *module_name, const char *msg)
{
	pr_warn("%s [%s] WARN: %s\n", log_prefix, module_name, msg);
}
EXPORT_SYMBOL_GPL(ep_log_warn);

static int __init ep_logger_init(void)
{
	pr_info("ep_logger: ready, prefix='%s'\n", log_prefix);
	return 0;
}

static void __exit ep_logger_exit(void)
{
	pr_info("ep_logger: unloaded\n");
}

module_init(ep_logger_init);
module_exit(ep_logger_exit);

Gathering System Information from Inside a Kernel Module

A common need — especially during debugging — is to print useful system information from within a kernel module. The kernel provides several ways to do this. Knowing these is part of writing practical Linux kernel modules.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/utsname.h>    /* for utsname() */
#include <linux/cpumask.h>    /* for num_online_cpus() */
#include <linux/mm.h>         /* for si_meminfo() */

MODULE_LICENSE("GPL");

static int __init sysinfo_init(void)
{
	struct sysinfo si;

	/* Kernel version and hostname */
	pr_info("sysinfo: kernel version = %s\n", utsname()->release);
	pr_info("sysinfo: hostname       = %s\n", utsname()->nodename);
	pr_info("sysinfo: machine arch   = %s\n", utsname()->machine);

	/* CPU count */
	pr_info("sysinfo: online CPUs    = %d\n", num_online_cpus());
	pr_info("sysinfo: possible CPUs  = %d\n", num_possible_cpus());

	/* Memory info */
	si_meminfo(&si);
	pr_info("sysinfo: total RAM      = %lu pages (%lu MB)\n",
	        si.totalram,
	        (si.totalram * si.mem_unit) / (1024 * 1024));
	pr_info("sysinfo: free RAM       = %lu pages (%lu MB)\n",
	        si.freeram,
	        (si.freeram * si.mem_unit) / (1024 * 1024));

	return 0;
}

static void __exit sysinfo_exit(void) { }

module_init(sysinfo_init);
module_exit(sysinfo_exit);
# Load and check output
sudo insmod sysinfo.ko
sudo dmesg | tail -10
# sysinfo: kernel version = 6.8.0-51-generic
# sysinfo: hostname       = my-dev-board
# sysinfo: machine arch   = aarch64
# sysinfo: online CPUs    = 4
# sysinfo: possible CPUs  = 4
# sysinfo: total RAM      = 2031616 pages (7936 MB)
# sysinfo: free RAM       = 1200000 pages (4687 MB)

Why Floating Point Is Not Allowed in Linux Kernel Code

This surprises many developers coming from userspace. You cannot use float or double in Linux kernel module code. If you try, the compiler will allow it but the behavior at runtime will be incorrect or the kernel will crash.

The Technical Reason

When a userspace process uses floating point operations, the CPU’s Floating Point Unit (FPU) registers are automatically saved and restored as part of context switches (the kernel saves the FPU state as part of the process’s register context). In the kernel itself, this automatic FPU save/restore is not in effect. If kernel code modifies the FPU registers and a context switch happens, the userspace process that gets scheduled next will find its FPU state corrupted — leading to wrong results or crashes in that process.

Why FPU Usage in Kernel Code is Dangerous
Without FPU Guard in Kernel Correct Approach
  1. Kernel code starts float calculation
  2. FPU registers modified
  3. Context switch happens mid-calculation
  4. Userspace process resumes
  5. Userspace finds FPU state corrupted ✘
  6. Incorrect math results / crash in userspace
  1. Use only integer arithmetic in kernel
  2. Use fixed-point math for fractional values
  3. Use kernel_fpu_begin() / kernel_fpu_end() if absolutely necessary (x86 only)
  4. No FPU register corruption risk ✔

What to Use Instead of Floating Point

Almost all cases where you think you need floating point in kernel code can be solved with integer arithmetic and fixed-point math:

/* === Userspace approach (NOT allowed in kernel) === */
float temperature = raw_adc_value * 0.0625;  /* WRONG: no float in kernel */

/* === Kernel approach: scale up, do integer math, scale down === */
/*
 * Example: ADC reads raw value, 1 LSB = 0.0625 degrees C
 * Instead of multiplying by 0.0625, multiply by 10000 and
 * divide by 160000 (= 10000 / 0.0625) to get millidegrees
 */
int raw = 512;  /* example ADC reading */

/* Represent 0.0625 as 625/10000 */
int temp_millidegrees = (raw * 625) / 10;
/* = 512 * 625 / 10 = 32000 millidegrees = 32.000 degrees C */

pr_info("Temperature: %d.%03d degrees C\n",
        temp_millidegrees / 1000,
        temp_millidegrees % 1000);
/* Output: Temperature: 32.000 degrees C */
/* Another common pattern: percentage calculation without float */
/* percentage = (value * 100) / total */
unsigned long used = 3072;   /* bytes used */
unsigned long total = 8192;  /* total bytes */
unsigned int pct = (used * 100) / total;
pr_info("Usage: %u%%\n", pct);  /* Usage: 37% */
Note: On x86, the kernel does provide kernel_fpu_begin() and fpu_end() for cases where float is absolutely necessary (like certain crypto or SIMD operations). However, this is an advanced technique that saves/restores FPU state manually and adds significant overhead. For sensor drivers, data conversion, or general logic, always use integer arithmetic.

Common Mistakes with Module Parameters and Module Stacking

  • Using non-static variables for module parameters: Module parameter variables should always be declared static. This limits their scope to the module’s translation unit and avoids symbol conflicts with the global kernel namespace.
  • Not providing MODULE_PARM_DESC: Always document your parameters with MODULE_PARM_DESC(). This description shows up in modinfo output and is essential for users of your module to understand what each parameter does.
  • Loading consumer before helper: If you use insmod directly, you must load dependencies manually in the right order. Use modprobe instead — it reads the dependency database and loads modules in the correct sequence automatically.
  • Forgetting extern declarations for stacked symbols: When a module uses an exported symbol from another module, declare it as extern in the consumer’s source. Without this, the compiler will warn about implicit function declarations.
  • Using float for sensor math: Always convert to scaled integer representation. This is a discipline that experienced kernel developers apply automatically.

Performance Considerations

  • Module parameter reads in hot paths: Reading a module parameter variable in a very frequently called function (interrupt handler, packet processing loop) is fine — it is just a global variable read. However, avoid making the parameter itself a sysfs-backed value if the read path is ultra-latency-sensitive.
  • Module stacking overhead: There is essentially no overhead to calling an exported function from another module. Once loaded, the symbol is resolved to a direct function pointer — there is no dynamic lookup at call time.
  • Integer vs float: Integer arithmetic in the kernel is inherently faster than the FPU-save/restore dance required for floating point, making the “no float” rule a performance benefit as well as a correctness requirement.

Key Takeaways

  • module_param() and module_param_array() let you pass configuration values to kernel modules at load time, and optionally change them at runtime via /sys/module/.
  • Always use appropriate sysfs permissions for module parameters — never world-writable.
  • Module stacking allows one kernel module to export functions for other modules to use, using EXPORT_SYMBOL_GPL(). This is how kernel subsystems like I2C, SPI, and USB are organized.
  • Floating point arithmetic is not safe in kernel code. Use scaled integer math instead.
  • Use modprobe rather than insmod for modules with dependencies — it handles load order automatically using the dependency database built by depmod.
  • Gathering system info (kernel version, CPU count, memory) from within a module is straightforward using utsname(), num_online_cpus(), and si_meminfo().

Frequently Asked Questions

Q1. Can I change a module parameter while the module is running?
Yes, if the parameter was declared with a writable sysfs permission (like 0644). Write the new value to /sys/module/<module_name>/parameters/<param_name>. The change takes effect immediately for the next read of that variable by the module. Be careful with parameters that affect hardware state — changing them at runtime may require additional synchronization logic in your module.
Q2. What is the difference between EXPORT_SYMBOL and EXPORT_SYMBOL_GPL?
EXPORT_SYMBOL() makes a symbol available to any kernel module, including proprietary (non-GPL) ones. EXPORT_SYMBOL_GPL() restricts the symbol to GPL-licensed modules only. The kernel enforces this at load time — if a non-GPL module tries to use a GPL-only symbol, the load is refused with an error about license taint. Most core kernel subsystem exports use EXPORT_SYMBOL_GPL().
Q3. Can module stacking cause circular dependencies?
The Linux kernel’s module loader does not allow circular dependencies. If module A depends on module B which depends on module A, neither can be loaded. Design your module dependencies as a directed acyclic graph (DAG). If you find yourself needing circular dependencies, it usually means the shared code should be restructured into a third, independent helper module.
Q4. Is there an equivalent of #include for sharing code between kernel modules without stacking?
Yes — you can put shared code in a header file that both modules include, and compile that code into each module. The downside is code duplication in memory (two copies of the same code loaded). Module stacking solves this by having a single copy of the shared code in the helper module. For small utility functions, either approach works. For large shared subsystems, module stacking is the right pattern.
Q5. Why does the kernel allow kernel_fpu_begin() if float is so dangerous?
kernel_fpu_begin() explicitly saves the current FPU state before using floating point or SIMD instructions, and kernel_fpu_end() restores it. This makes FPU usage safe by preventing corruption of the task’s FPU state. It is used in performance-critical kernel subsystems like AES-NI cryptography (which uses AES hardware via SSE registers) and video codec drivers. It is not available on all architectures and should only be used when there is a strong performance justification and no integer alternative.
Q6. How many module parameters can a kernel module have?
There is no hard-coded limit on the number of module parameters. Practically, you should keep parameters to what is genuinely needed for configuration. Having too many parameters makes a module hard to use and maintain. Common well-designed drivers have between 2 and 10 parameters. Very complex drivers (like some WiFi drivers) may have 20 or more.

Authoritative References

Free Linux Kernel Programming — Keep Learning

This is part of EmbeddedPathashala’s completely free Linux kernel development course. No fees, no login. Just practical kernel programming knowledge updated for Linux 6.x.

View Full Course Index Subscribe on YouTube

Leave a Reply

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