Creating Custom Kernel Modules

2024/2024-08-11 · 4 min read

Linux kernel modules are pieces of code that can be dynamically loaded and unloaded into the kernel upon demand. They extend the functionality of the kernel without the need to reboot the system. In this guide, we’ll explore how to create, compile, and load custom kernel modules, providing you with the knowledge to extend and customize your Linux system.

Table of Contents

  1. Introduction to Kernel Modules
  2. Setting Up the Development Environment
  3. Creating a Basic Kernel Module
  4. Compiling and Loading the Module
  5. Advanced Techniques
  6. Best Practices and Debugging
  7. Conclusion

Introduction to Kernel Modules

Kernel modules are object files that contain code to extend the running kernel. They can be used to add support for new hardware, file systems, or implement new features without recompiling the entire kernel.

Key advantages of kernel modules include:

  • Dynamic loading and unloading
  • Smaller kernel size
  • Easier debugging and development
  • Flexibility in system configuration

Setting Up the Development Environment

Before we start, ensure you have the necessary tools installed:

sudo apt-get update
sudo apt-get install build-essential linux-headers-$(uname -r)

This installs the GCC compiler, make, and the kernel headers for your current kernel version.

Creating a Basic Kernel Module

Let’s create a simple “Hello, World!” kernel module. Create a file named hello.c:

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");
MODULE_VERSION("0.1");

static int __init hello_init(void) {
    printk(KERN_INFO "Hello, World!\n");
    return 0;
}

static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye, World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

This module does the following:

  • Includes necessary header files
  • Defines module metadata (license, author, description, version)
  • Implements init and exit functions
  • Registers these functions using module_init and module_exit

Compiling and Loading the Module

To compile the module, we need a Makefile:

obj-m += hello.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Now, compile the module:

make

This will generate several files, including hello.ko, which is our kernel module.

To load the module:

sudo insmod hello.ko

To verify it’s loaded:

lsmod | grep hello

To see the “Hello, World!” message:

dmesg | tail

To unload the module:

sudo rmmod hello

Advanced Techniques

1. Module Parameters

You can make your module configurable by adding parameters:

#include <linux/moduleparam.h>

static int count = 1;
module_param(count, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(count, "Number of times to print Hello");

static int __init hello_init(void) {
    int i;
    for (i = 0; i < count; i++)
        printk(KERN_INFO "Hello, World!\n");
    return 0;
}

Now you can pass parameters when loading:

sudo insmod hello.ko count=5

2. Interacting with Hardware

Kernel modules can interact directly with hardware. Here’s a simple example that reads from a GPIO pin:

#include <linux/gpio.h>

#define GPIO_PIN 17

static int __init gpio_init(void) {
    int value;
    
    if (gpio_request(GPIO_PIN, "example") < 0) {
        printk(KERN_ALERT "Failed to request GPIO\n");
        return -1;
    }
    
    gpio_direction_input(GPIO_PIN);
    value = gpio_get_value(GPIO_PIN);
    printk(KERN_INFO "GPIO %d value is %d\n", GPIO_PIN, value);
    
    gpio_free(GPIO_PIN);
    return 0;
}

3. Creating Proc Entries

Kernel modules can create entries in the /proc filesystem to communicate with user space:

#include <linux/proc_fs.h>
#include <linux/seq_file.h>

static int hello_proc_show(struct seq_file *m, void *v) {
    seq_printf(m, "Hello from kernel module\n");
    return 0;
}

static int hello_proc_open(struct inode *inode, struct file *file) {
    return single_open(file, hello_proc_show, NULL);
}

static const struct proc_ops hello_proc_fops = {
    .proc_open = hello_proc_open,
    .proc_read = seq_read,
    .proc_lseek = seq_lseek,
    .proc_release = single_release,
};

static int __init hello_init(void) {
    proc_create("hello_proc", 0, NULL, &hello_proc_fops);
    return 0;
}

static void __exit hello_exit(void) {
    remove_proc_entry("hello_proc", NULL);
}

This creates a /proc/hello_proc file that outputs “Hello from kernel module” when read.

Best Practices and Debugging

  1. Use pr_* macros: Instead of printk, use pr_info, pr_err, etc., for better log level control.

  2. Error handling: Always check return values and handle errors gracefully.

  3. Memory management: Be cautious with memory allocation. Always free allocated memory in the exit function.

  4. Locking: Use appropriate locking mechanisms when accessing shared resources.

  5. Debugging: Use printk for debugging, but remember to remove or disable debug messages in production code.

  6. Code style: Follow the Linux kernel coding style guide for consistency.

  7. Testing: Thoroughly test your module under various conditions. Use tools like KASAN (Kernel Address Sanitizer) for detecting memory errors.

Conclusion

Creating custom Linux kernel modules is a powerful way to extend the functionality of the Linux system. While it requires careful programming and thorough understanding of kernel concepts, it opens up possibilities for deep system customization and optimization. I will share some cool modules in later posts.

Remember, bugs in kernel code can crash the entire system, so always test your modules thoroughly before deploying them in production environments.