Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Building Your First Loadable Kernel Module

Recommended resources

Introduction

In our last lesson, you successfully configured, compiled, and installed a custom Linux kernel. You now have first-hand experience with the Kconfig and Kbuild systems, which are central to kernel development. Today, we will build on that foundation. Instead of building the entire kernel, we will create a small, independent piece of code that can be dynamically loaded into and unloaded from your running kernel.

This lesson addresses the learning outcome: Write, compile, and load a basic loadable kernel module that interacts with kernel APIs. This is a cornerstone skill for any kernel developer, as modules are the primary mechanism for extending kernel functionality, most notably for writing device drivers—a topic highly relevant to your embedded systems background.

We will cover:

  1. The anatomy and lifecycle of a "Hello, World" kernel module.
  2. How to compile and manage your module.
  3. How to interact with core kernel data structures, including a practical debugging scenario.
  4. An introduction to creating a character device, the simplest form of a driver interface.

1. Your First Kernel Module: "Hello, World"

A Loadable Kernel Module (LKM) is a piece of object code that can be inserted into and removed from the kernel on demand. This modularity is what allows Linux to support a vast array of hardware and features without requiring a monolithic, statically-linked kernel for every possible configuration.

Let's begin by creating the simplest possible module. This video provides an excellent, step-by-step walkthrough.

Let's code a Linux Driver - 1: Hello World Linux Kernel Module

The video 'Let's code a Linux Driver - 1: Hello World Linux Kernel Module' by Johannes 4GNU_Linux will guide you through the entire process of creating, compiling, and running your first kernel module.

Watch the video from the beginning until 11:53. The presenter will cover everything you need for this first step. Follow along and create the files on your own system. Key points to focus on are: The purpose of the module_init and module_exit macros. The use of printk for logging from kernel space. The structure of the minimal Makefile and what the obj-m variable does. The commands insmod, lsmod, dmesg, and rmmod.

Core Components

As you saw in the video, a basic kernel module consists of a few key parts:

  • Header Files:
    • #include <linux/module.h>: Required by every module.
    • #include <linux/init.h>: Defines the module_init and module_exit macros.
  • Initialization and Cleanup Functions:
    • A function marked with module_init(your_init_function) is executed when you load the module. It must return 0 on success or an error code on failure.
    • A function marked with module_exit(your_exit_function) is executed just before the module is unloaded.
  • Metadata Macros:
    • MODULE_LICENSE("GPL"): Declares the software license. This is not just a comment; the kernel checks this string, and modules without a GPL-compatible license cannot access all kernel symbols.
    • MODULE_AUTHOR(...) and MODULE_DESCRIPTION(...): Provide descriptive information visible with tools like modinfo.
  • Makefile: A simple Makefile is used to hook into the kernel's build system. The key line is obj-m += mymodule.o. This tells Kbuild to compile mymodule.c and link it as a loadable module (mymodule.ko). The rest of the Makefile points to the location of your kernel source/headers to find the necessary build infrastructure.

After following the video, you should have a working mymodule.c and Makefile, and you should be able to successfully compile, load, and unload your "Hello, World" module.


2. Interacting with Kernel APIs

Printing "Hello" is a start, but the real power of modules comes from their ability to interact with the rest of the kernel. Modules run in kernel space with full privileges, meaning they can call any exported kernel function and access any kernel data structure.

Let's modify our module to do something more interesting: iterate through the list of all running processes on the system. This requires accessing the kernel's internal process list.

The following tutorial provides the code and a fantastic, interview-relevant debugging challenge.

Assignment 3: Linux Kernel Module Tutorial (v0.15)

This PDF document, 'Linux Kernel Module Tutorial', will guide you through modifying your module to access the task_struct of each process.

Please read and follow the instructions in Section 3: Count the number of running Linux tasks and Section 4: Inspecting Linux Task Codesize. Key steps: Add the #include <linux/sched/signal.h> header. Implement the function to count processes using the for_each_process macro, which iterates over task_struct entries. Crucially, follow the steps in Section 4 to inspect the process code size. When you first load this modified module, it will cause a kernel panic! This is intentional. Examine the dmesg output carefully to understand the 'unable to handle kernel NULL pointer dereference' error. Follow the guide to fix the bug by adding a check for thechild->mm. This exercise of causing, diagnosing, and fixing a kernel bug is extremely valuable.

This exercise demonstrates a critical aspect of kernel programming: you must be defensive. Not all data structures are always valid. In this case, kernel threads do not have a user-space memory map, so their task_struct->mm pointer is NULL. Attempting to dereference this NULL pointer causes an immediate system crash. Your experience debugging low-level issues in embedded systems will make the importance of this check immediately apparent.

For a deeper dive (Optional):
You can extend this further by implementing Section 5: Inspecting Linux Task Datasize and Heapsize from the same PDF (LINK, section 4). This will give you more practice navigating the mm_struct and understanding process memory layout from the kernel's perspective.


3. Creating a Basic Character Device

One of the most common applications for a kernel module is to act as a device driver. A device driver is the bridge between hardware and the user-space applications. In Linux, these interfaces are often presented as files in the /dev directory.

The simplest type of driver is a character driver. It exposes a character-stream interface, meaning user-space programs can open(), read(), write(), and close() it just like a regular file.

Linux Character Device Driver Architecture Overview
This diagram shows the path from a user-space application to a character driver. The application's file operations (like `read`) are passed through the Virtual File System (VFS), which directs them to the functions you implement in your kernel module. Your module can then interact with hardware or, in our case, simply manage an internal buffer.

We will now transform our module into a minimal character device. It won't control any real hardware; instead, when a user reads from our device file, we will simply send back a message from a kernel buffer.

This video builds directly on the "Hello, World" example to implement this functionality.

Let's code a Linux Driver: 5 - Create a Character Device in a Linux Driver

The video 'Let's code a Linux Driver: 5 - Create a Character Device' will guide you through the process of registering a character device and implementing its read operation.

Follow the video from 01:33 to 13:08. You will modify your module to: Register a character device: Use register_chrdev() in your init function to dynamically get a 'major number' and register your device. Use unregister_chrdev() in the exit function. Define file operations: Create a struct file_operations and implement a basic my_read function. This struct tells the kernel which of your functions to call for read, write, open, etc. Link them together: Point the read field in your file_operations struct to your my_read function. Test from user-space: After loading the module, check /proc/devices, create the device file with mknod, and use cat to trigger your read function. Observe the printk message in dmesg.

To create a character device, you are interacting with a different set of kernel APIs, centered around the struct file_operations. This structure is a collection of function pointers. By filling it out, you are effectively telling the VFS, "When someone tries to read from my device, call this function."

For more detailed background information on the data structures involved, you can refer to the "Character device drivers" documentation (LINK). The video provides the practical steps, while the documentation provides the theoretical underpinnings for concepts like:

  • struct file_operations, struct file, and struct inode (section 2)
  • The registration functions register_chrdev_region, cdev_add, etc. (section 4)
  • Safely copying data to and from user-space with copy_to_user (sections 5 and 7)

Conclusion

In this lesson, you have taken a significant step from being a user of the kernel to a developer for the kernel. You wrote code that runs in kernel space, compiled it against the kernel's build system, and managed its lifecycle in a running system.

Key Takeaways:

  • Loadable Kernel Modules (LKMs) are the standard way to add code, like device drivers, to a running Linux kernel.
  • They have a standard structure with module_init, module_exit, and MODULE_LICENSE.
  • The Makefile for an external module is simple, leveraging the powerful Kbuild system you're already familiar with.
  • Modules can call kernel functions and access kernel data, but must do so carefully to avoid system instability (e.g., checking for NULL pointers).
  • Character devices are a fundamental driver model that creates a file-based interface between user-space applications and your kernel code via the file_operations struct.

In our next lesson, we will shift our focus from modifying a running kernel to understanding how it starts. We will describe the major stages of the Linux boot process, from bootloader handoff to the first user-space process, tracing the kernel's first moments of life.