Your First Linux Kernel Module: Writing, Building & Loading

Write Your First Linux Kernel Module – Free Linux Device Driver Course | EmbeddedPathashala

Your First Linux Kernel Module
Part 2 — Writing, Building & Loading a Kernel Module on Linux 6.x
🆓 100% Free
💻 Hands-On
🐧 Linux 6.x
⏱ ~25 min read
🔑 Key Topics in This Part
module_init / module_exit MODULE_LICENSE __init and __exit macros Kernel Module Makefile insmod / rmmod dmesg output Module Parameters GPL License

In Part 1 you understood the theory behind kernel modules. Now it is time to actually write one. By the end of this part, you will have compiled a working kernel module, loaded it into a running Linux 6.x kernel, and verified its output in the kernel log.

1. Anatomy of a Minimal Kernel Module

Every kernel module — no matter how complex — has two mandatory ingredients: an init function called when the module loads, and an exit function called when it unloads. There are also mandatory license and metadata declarations.

Here is the simplest possible kernel module, explained line by line:

/*
 * mymodule.c — Minimal Linux Kernel Module
 * Tested on Linux 6.x
 */

#include <linux/module.h>   /* Required for all kernel modules */
#include <linux/init.h>     /* Provides __init and __exit macros */
#include <linux/printk.h>   /* Provides pr_info() and friends */

/* Module metadata — appears in modinfo output */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A minimal kernel module example");
MODULE_VERSION("1.0");

/*
 * my_module_init — called when module is loaded
 * Must return 0 on success, negative errno on failure.
 * __init tells the kernel it can free this function's memory after boot.
 */
static int __init my_module_init(void)
{
    pr_info("mymodule: loaded successfully\n");
    return 0;   /* 0 = success; any nonzero value aborts loading */
}

/*
 * my_module_exit — called when module is unloaded
 * Must clean up everything allocated in init.
 * __exit marks this as cleanup code — not needed if module is built-in.
 */
static void __exit my_module_exit(void)
{
    pr_info("mymodule: unloaded\n");
}

/* Register the init and exit functions with the kernel */
module_init(my_module_init);
module_exit(my_module_exit);

2. Understanding Every Line

#include <linux/module.h>

This is the most important header for any kernel module. It provides the module_init() and module_exit() macros, the MODULE_* metadata macros, and the exported kernel symbol infrastructure. Without this, your file is not a kernel module.

#include <linux/init.h>

Provides the __init and __exit section attributes. These are important memory optimization hints to the kernel (explained below).

#include <linux/printk.h>

Provides pr_info(), pr_err(), pr_warn() and the full family of kernel logging functions. In modern Linux kernel code (6.x), prefer these pr_*() wrappers over calling printk() directly — they are cleaner and support pr_fmt() prefixing.

MODULE_LICENSE("GPL")

This is not optional. It tells the kernel what license governs your module. Setting it to "GPL" allows your module to use GPL-only exported symbols — functions that are marked EXPORT_SYMBOL_GPL(). Many important kernel subsystem functions are GPL-only. Setting any non-GPL license restricts which kernel internals you can call, and the kernel will emit a “tainted kernel” warning.

Valid values: "GPL", "GPL v2", "GPL and additional rights", "Dual MIT/GPL", "Dual BSD/GPL", "Proprietary".

The __init macro

This is a section attribute that places the init function into a special .init.text section in the binary. After the module finishes its initialization (or after the kernel boots for built-in modules), the kernel frees this memory. This means your init function’s code does not stay in RAM after it has done its job. Use __init on your init function and __initdata on any data that is only needed during initialization.

The __exit macro

Places the exit function in .exit.text. For modules that are compiled statically into the kernel (=y), there is no way to unload them, so the exit function is never called. The kernel can therefore discard it entirely for built-in modules. For loadable modules, __exit is preserved normally so it can be called at rmmod time.

module_init() and module_exit()

These macros register your init and exit functions with the kernel module framework. You can name your functions anything you like — as long as you pass the correct function pointer here. The kernel calls the registered init function when the module is inserted and the exit function when it is removed.

3. The Return Value of the Init Function

The init function must return an integer. Return 0 on success. Return a negative error code (-ENOMEM, -ENODEV, -EINVAL, etc.) if initialization fails. When you return a nonzero value, the kernel aborts the load — insmod will print an error and the module will not be inserted.

static int __init my_module_init(void)
{
    void *buf;

    buf = kmalloc(1024, GFP_KERNEL);  /* allocate 1KB kernel memory */
    if (!buf) {
        pr_err("mymodule: failed to allocate memory\n");
        return -ENOMEM;  /* abort loading — insmod will report an error */
    }

    pr_info("mymodule: initialized, buffer at %p\n", buf);
    return 0;
}

static void __exit my_module_exit(void)
{
    /* In real code: kfree(buf) here */
    pr_info("mymodule: cleaned up\n");
}
Rule: Every resource you allocate in init must be freed in exit. If init allocates memory, registers a device, or creates a file in /proc, then exit must do the corresponding cleanup — even if init failed partway through. Forgetting this causes kernel memory leaks that persist until the next reboot.

4. Writing the Module Makefile

Kernel modules are not compiled like regular C programs with gcc mymodule.c -o mymodule. They must be compiled using the kernel build system (kbuild), which sets up all the right compiler flags, header paths, and section attributes automatically.

Create a file named Makefile in the same folder as your mymodule.c:

# Makefile for a simple out-of-tree kernel module

# obj-m tells kbuild to build mymodule.c as a loadable module (.ko)
obj-m += mymodule.o

# Path to the kernel build directory for the currently running kernel
KDIR := /lib/modules/$(shell uname -r)/build

# Current working directory
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
Important: The indentation before $(MAKE) in a Makefile must be a real Tab character, not spaces. If you use spaces, make will fail with a confusing “missing separator” error. Every text editor has a “insert tab” option — make sure you use it here.
How kbuild Compiles Your Module
Your source directory
mymodule.c + Makefile
make -C /lib/modules/…/build M=$(PWD)
kbuild enters kernel build directory, compiles M= as a module
mymodule.ko
Ready to load with insmod
Side outputs: mymodule.o  ·  mymodule.mod.c  ·  mymodule.mod.o  ·  Module.symvers  ·  modules.order  ·  .tmp_versions/

5. Building and Testing Your Module

With mymodule.c and Makefile in the same folder, follow these steps:

1Build the module
make

# Expected output (Linux 6.x):
# make -C /lib/modules/6.8.0-45-generic/build M=/home/user/mymodule modules
# CC [M]  /home/user/mymodule/mymodule.o
# MODPOST /home/user/mymodule/Module.symvers
# CC [M]  /home/user/mymodule/mymodule.mod.o
# LD [M]  /home/user/mymodule/mymodule.ko
# BTF [M] /home/user/mymodule/mymodule.ko
2Check the generated .ko file
ls -lh mymodule.ko
modinfo mymodule.ko

# Expected modinfo output:
# filename:    /home/user/mymodule/mymodule.ko
# description: A minimal kernel module example
# author:      Your Name
# license:     GPL
# version:     1.0
# vermagic:    6.8.0-45-generic SMP preempt mod_unload
3Load the module
sudo insmod mymodule.ko

# No output means success (errors go to stderr)
# Check the kernel log:
dmesg | tail -5

# Expected:
# [12345.678901] mymodule: loaded successfully
4Verify it is loaded
lsmod | grep mymodule

# Expected:
# mymodule   16384  0
# (name, size in bytes, use count)
5Unload the module
sudo rmmod mymodule
dmesg | tail -5

# Expected:
# [12350.123456] mymodule: unloaded
6Clean build artifacts
make clean

# Removes: *.ko, *.o, *.mod.c, *.mod.o, Module.symvers,
#          modules.order, .tmp_versions/

6. Passing Parameters to a Module

You can pass configuration values to a module at load time using module_param(). This is how device drivers accept runtime configuration like IRQ numbers, I/O port addresses, or debug levels.

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Module parameter example");

/* Declare a parameter: name, type, permissions in /sys/module/ */
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Set debug verbosity level (0=off, 1=basic, 2=verbose)");

static char *device_name = "mydevice";
module_param(device_name, charp, 0444);
MODULE_PARM_DESC(device_name, "Name of the target device");

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

static void __exit param_module_exit(void)
{
    pr_info("param_module: unloaded\n");
}

module_init(param_module_init);
module_exit(param_module_exit);

To pass parameters at load time:

# Pass values using insmod:
sudo insmod param_module.ko debug_level=2 device_name=uart0

# Kernel log will show:
# param_module: loaded — debug_level=2, device_name=uart0

# Parameters also appear in sysfs while the module is loaded:
cat /sys/module/param_module/parameters/debug_level
# Output: 2

# If permission is 0644, you can change it at runtime:
echo 1 | sudo tee /sys/module/param_module/parameters/debug_level
module_param() Permission Flags
Permission Value Meaning Use Case
0 No sysfs entry created Internal-only, not user-visible
0444 Readable by all, not writable Read-only config (device names, IDs)
0644 Root can write, all can read Runtime-tunable (debug levels, thresholds)
0600 Root only read/write Sensitive parameters

7. Supported Parameter Types

/* Integer types */
static int      my_int;    module_param(my_int,    int,    0644);
static long     my_long;   module_param(my_long,   long,   0644);
static short    my_short;  module_param(my_short,  short,  0644);

/* Unsigned types */
static uint     my_uint;   module_param(my_uint,   uint,   0644);
static ulong    my_ulong;  module_param(my_ulong,  ulong,  0644);

/* Boolean: accepts 0/1, y/n, Y/N */
static bool     my_bool;   module_param(my_bool,   bool,   0644);

/* String (char pointer) */
static char    *my_str;    module_param(my_str,    charp,  0444);

/* Arrays of integers */
static int my_arr[4];
static int my_arr_cnt;
module_param_array(my_arr, int, &my_arr_cnt, 0644);

/* Load example: */
/* sudo insmod mymod.ko my_arr=10,20,30,40 */
/* my_arr_cnt will be set to 4 automatically */

8. Module Init/Exit Lifecycle in Detail

Complete Module Lifecycle — What the Kernel Does at Each Stage
📥 sudo insmod mymodule.ko
  1. Kernel reads .ko ELF binary
  2. Verifies vermagic string matches running kernel
  3. Allocates kernel memory for module code+data
  4. Copies sections into kernel memory
  5. Resolves undefined symbols from kernel exports
  6. Performs ELF relocation fixups
  7. Calls your init function
  8. Module appears in /proc/modules and lsmod
📤 sudo rmmod mymodule
  1. Kernel checks use count — must be 0
  2. Calls your exit function
  3. Waits for exit function to return
  4. Removes module from /proc/modules
  5. Frees kernel memory allocated for module
  6. Unregisters all exported symbols

9. Common Errors and How to Fix Them

❌ insmod: ERROR: could not insert module — Invalid module format

Cause: The module was compiled against a different kernel version than what is running. The vermagic check failed.

Fix: Recompile your module. Run make clean then make again while the target kernel is booted. Check that uname -r output matches the path in your Makefile.

❌ insmod: ERROR: could not insert module — Unknown symbol

Cause: Your module uses a function that the kernel does not export, or a prerequisite module is not loaded.

Fix: Check that the function is exported with EXPORT_SYMBOL() in kernel source. If it is in another module (e.g., i2c-core), load that module first. The depends line in modinfo output shows required modules.

❌ rmmod: ERROR: Module mymodule is in use

Cause: Something holds a reference to the module (an open file descriptor, another module depending on it, etc.).

Fix: Close any open device files, unload dependent modules first, or check lsmod to see which module holds the dependency.

❌ make: *** missing separator. Stop.

Cause: The indentation in your Makefile uses spaces instead of a Tab character.

Fix: Replace all leading spaces before $(MAKE) with a single Tab character.

🎯 Interview Questions — Writing Kernel Modules

Q1. What happens if the init function of a kernel module returns a non-zero value?
The kernel treats any non-zero return from the init function as a failure. The module loading is aborted, the module is not inserted, and insmod reports an error. No cleanup via the exit function is called in this case. The return value should be a standard negative errno (e.g., -ENOMEM, -ENODEV).
Q2. What does MODULE_LICENSE(“GPL”) actually do at the kernel level?
It sets a flag in the module’s ELF metadata. When the module loads, the kernel checks this flag. If it is not a GPL-compatible license, the kernel marks itself as “tainted” (shown in dmesg and /proc/sys/kernel/tainted). More critically, the module is restricted from calling EXPORT_SYMBOL_GPL() symbols — functions that the kernel restricts to GPL-licensed modules only.
Q3. What is the purpose of the __init macro and where does it put the init function in memory?
__init is a GCC section attribute that places the annotated function into the .init.text ELF section. After module initialization is complete, the kernel frees this section’s memory. This reduces the module’s runtime memory footprint because init code is rarely needed again after the module is loaded.
Q4. How do you make a kernel module load automatically at system boot?
Install the .ko file into /lib/modules/$(uname -r)/ under the appropriate subdirectory, run depmod -a to update dependency maps, then add the module name (without .ko) to /etc/modules-load.d/mymod.conf. systemd reads this file at boot and uses modprobe to load the listed modules. For parameters, add a file like /etc/modprobe.d/mymod.conf with “options mymod param=value”.
Q5. What is the difference between module_param() and module_param_array()?
module_param() declares a single scalar parameter (int, bool, string, etc.). module_param_array() declares an array parameter where the caller passes comma-separated values at load time. It takes an additional pointer argument that receives the actual count of values provided, which lets you safely iterate only over the values the user actually passed.

✅ Part 2 Summary

  • Every module needs an init function, an exit function, and MODULE_LICENSE
  • Use __init and __exit for memory efficiency
  • The init function must return 0 on success, negative errno on failure
  • Modules are built using the kernel’s kbuild system — never plain gcc
  • The Makefile’s obj-m variable tells kbuild what to compile as a module
  • Use module_param() to accept configuration values at load time
  • Always free in exit() everything you allocated in init()
Great work! Your module runs in the kernel.

In Part 3, you will master kernel logging with printk and the pr_*() family, understand log levels, use rate-limited logging, and build a production-quality Makefile.

→ Continue to Part 3: printk, Logging & Makefile
← Back to Part 1

EmbeddedPathashala — Free Linux Kernel Programming Course | Updated for Linux 6.x

Leave a Reply

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