Develop a kernel module in Linux

Cover image of development of a Linux kernel module

We have seen previously how to develop and integrate a system call into the Linux kernel. Now we are going to look at how to develop another form of code execution in ring 0 (kernel-land): the Linux kernel module system.

We will see how to develop a kernel module and in addition, we will see how to dynamically intereract with our Linux module from the user-land using “misc devices”.

Note
Don’t worry: it’s totally different from a system call, both in terms of how it works and how to integrate/test it. There are plenty of new concepts to learn here and no redundancy with the article on system calls.

1. Introduction

Before starting and so that we are on the same basis, we will explain a little what we are talking about here.

We are going to focus on the modules (or drivers in Windows language) of the Linux kernel. These are pieces of code in ring 0 (kernel-land) that can be loaded and unloaded from the kernel according to our needs.

Several modules are already loaded in your Linux kernel by default, in particular for the management of the touchpad, the camera, the microphone, the touchscreen, KVM, etc. You can also list all the Linux modules loaded with the following command.

$ lsmod
Module                  Size  Used by
rfcomm                 81920  4
cdc_mbim               20480  0
cdc_wdm                24576  1 cdc_mbim
cdc_ncm                45056  1 cdc_mbim
cdc_ether              20480  1 cdc_ncm
...

Some of these modules also create communication interfaces, often in /dev for character devices, as is the case of the KVM module with /dev/kvm.

We will therefore in this article develop our own Linux kernel module, load it, and communicate with it via an interface exposed in /dev.

2. Prerequisites to develop your own kernel module

If you want to follow the development and test by yourself, there are a few prerequisites.

  • Operating system running on a relatively recent Linux kernel.
  • Usual development tools (gcc, make, …)
  • A text editor (vim, VSCode, …)

Contrary to the article on system calls, here we are not going to recompile the whole kernel or even boot on another kernel. So don’t worry, your system risks (almost!) nothing. Indeed, the only risk is to crash your system if you load a module whose code leads to a segmentation fault or exception that the kernel cannot handle. In this case, all you have to do is restart your machine.

3. Develop the kernel module

Tip
Good news, your default Linux environment is already ready for the development, compilation and loading of a new module. You don’t really have to do anything more to develop and compile a Linux kernel module. If you can compile C code then you’re good to go.

3.1. Simple development

$ tree my_module/
my_module/
└── my_module.c

0 directories, 1 file

Unlike the post on the system call, here we will stick to a very simple code so that everyone can understand what we are doing. We will now write in the C file the few basic lines that will constitute our module.

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ech0 <[email protected]>");
MODULE_DESCRIPTION("Hello World module");

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

static void __exit hello_cleanup(void) {
	printk(KERN_INFO "Cleaning up module.\n");
}

module_init(hello_init);
module_exit(hello_cleanup);
Explanation

A small explanation is needed.

  • First, we import the Linux kernel header files that we need for compiling our module.
  • Then, some kernel macros will allow us to specify metadata about our module: license, author, description.
  • Two minimal definitions of generic functions follow: they take the attributes __init and __exit, respectively called when the kernel is loaded or unloaded.
  • Finally, the module_init() and module_exit() functions will indicate our initialization and destruction functions to the kernel.
  • Our two functions will, for now, simply print text in the kernel logs using printk().

3.2. Compiling and loading the kernel module

We can now compile our module and load it into the kernel.

The following Makefile that we add in our working folder will allow us to correctly compile our module and clean up the various object files that could be generated.

FILE := "my_module"
obj-m += my_module.o

all:
	echo "Compiling $(FILE)..."
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:	
	echo "Cleaning modules..."
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
	rm -rf .$(FILE).ko.cmd .$(FILE).mod.o.cmd .$(FILE).o.cmd .cache.mk .tmp_versions $(FILE).ko $(FILE).o $(FILE).mod.c $(FILE).mod.o modules.order Module.symvers 2>&-

We now proceed to the compilation.

make

Now a whole bunch of files have been generated in the current folder. The one that interests us is the file with the .ko extension, in our case it is my_module.ko: this is the kernel object that we will load as a module using the insmod command.

sudo insmod my_module.ko
Warning
You will not be able to load an unsigned module into your kernel if you have Secure Boot enabled in your bios. I won’t talk about secure boot and Linux module and kernel signing in this article. You will find a lot of information on the internet.

We can use the dmesg command to see the loading of our module, with the display of the string “Hello World!” defined earlier in the initialization.

[309528.612844] Hello World !

Now we will remove the kernel module with the rmmod command and see the display of the string “Cleaning up module.” through the dmesg command.

$ sudo rmmod my_module
[309551.667600] Cleaning up module.
Success
Our module works as expected. You now know develop (write, compile, load and unload) your own module in the Linux kernel. This article could end there, but I also want to show you how to interact with this module dynamically via a misc device.

Now, if you wish, you can clean the object files with make clean.

4. Interaction with the kernel module

In this step, we are going to register a misc device to the kernel via the misc_register function. In order to do this, we are going to modify our module code a bit.

As a reminder, our module code, written in my_module.c, currently looks like this:

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ech0 <[email protected]>");
MODULE_DESCRIPTION("Hello World module");

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

static void __exit hello_cleanup(void) {
	printk(KERN_INFO "Cleaning up module.\n");
}

module_init(hello_init);
module_exit(hello_cleanup);

4.1. Registering the device

I will explain step by step the additions that we are going to make to the code. First, we’ll declare our misc device structure as a static global variable at the top of our C code:

static struct miscdevice my_dev;

This structure will be used for registering and deleting the misc device.

Next, we will, in the hello_init initialization function, assign some fields and register the misc device:

my_dev.minor = MISC_DYNAMIC_MINOR; // a dynamic minor number is requested
my_dev.name = "my_module_misc"; // name of the misc device
my_dev.fops = &my_fops; // operations structure
ret = misc_register(&my_dev); // registering the misc device

Now we will have to define the operations structure. It’s simple, we are defining a communication interface, so we have to specify what our module should do when we try to read from or write to it.

struct file_operations my_fops = {
	.read = hello_read,
	.write = hello_write
};

So we specify that when reading, it is the hello_read function that will be called, and when writing it will be hello_write.

Finally, we need to specify that the misc device should be destroyed when the module is unloaded from the kernel. So we call the following function in our hello_cleanup destruction function.

misc_deregister(&my_dev);

We add the few missing headers to use our misc device:

#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/fs.h>

To summarize, our code now looks like this:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/fs.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ech0 <[email protected]>");
MODULE_DESCRIPTION("Hello World module");

static struct miscdevice my_dev;

struct file_operations my_fops = {
	.read = hello_read,
	.write = hello_write
};

static int __init hello_init(void) {
	int ret;

	printk(KERN_INFO "Hello World !\n");

	my_dev.minor = MISC_DYNAMIC_MINOR;
	my_dev.name = "my_module_misc";
	my_dev.fops = &my_fops;
	ret = misc_register(&my_dev);

	return ret;
}

static void __exit hello_cleanup(void) {
	printk(KERN_INFO "Cleaning up module.\n");
	misc_deregister(&my_dev);
}

module_init(hello_init);
module_exit(hello_cleanup);

Yes, something is missing: the hello_read and hello_write functions, otherwise our code will not compile! We now need to define how this module should react when the user reads or writes to the misc device.

To keep things simple, let’s just decide that when writing to the device, our module will store a string of exactly 10 bytes in a static buffer. On a read operation, we’ll just return that string to the user.

It looks useless, but you’ll see that we already have plenty to do with it.

4.2. Write operation

Let’s start with the writing function.

Source code
static ssize_t hello_write(struct file *f, const char __user *s, size_t n, loff_t *o)
{
	int retval = -EINVAL;

	if (!f || !s)
		return -EFAULT;
	if (n != LEN)
		return -EINVAL;

	retval = copy_from_user(buf, s, LEN);

	if (retval)
		return -EFAULT;

	printk(KERN_INFO "I have successfully written %s in buffer.", buf);

	return LEN;
}
Explanation

A small explanation is needed.

  • Function prototype
    • struct file *f: file descriptor associated with the device
    • const char __user *s: user-land pointer of the buffer where the user wrote
    • size_t n: number of bytes written by the user in user-land
    • loff_t *o: current offset of the pointer in the write or read buffer
  • Basic checks
    • if (!f || !s): we check that the pointer to the file and the pointer to the user buffer are not null
    • if (n!= LEN): we check that the user has written exactly LEN bytes (size check)
  • Retrieval of data from the user land with the copy_from_user(buf, s, LEN) function. We copy the data pointed to by s in user-land, of size LEN, to buf in kernel-land. The returned value will be null if the function call is successful.
  • Verification of the success of the copy of user data with if (retval).
  • Returning the number of bytes read via return LEN. Attention, it is very important here to return the correct number of bytes read, which is LEN in our case: it is very easy to crash the kernel (unless you are fast enough to do an rmmod) by returning for example 0 which will have the effect of looping the function infinitely…

4.3. Read operation

Perfect, now it’s the turn of the reading function.

Source code
static ssize_t hello_read(struct file *f, char __user *s, size_t n, loff_t *o)
{
    if (!f || !s || !o)
        return -EFAULT;
    if (*o >= LEN)
        return 0;
    if (n > LEN)
        n = LEN;
    if (copy_to_user(s, &buf[*o], n))
        return -EFAULT;

    *o += n;

    return n;
}
Explanation

Again, a small explanation is needed… That said, I won’t explain again what is redundant with the write function.

  • Basic checks
    • Checking the loff_t *o offset: it is important here since we are going to use it.
    • Checking the current offset: if (*o >= LEN). This is because we need to return 0 when the user has finished reading the requested string. To do this, we rely on the current offset, or the pointer in the file, to define whether we are at the end or not. Warning: if this return is not properly managed, it can lead to infinite loops.
    • Checking the read size with if (n > LEN): we make sure that the user does not request more bytes than what we have stored in our buffer.
  • Writing the character string from the buffer to the user-land buffer. You will notice here that we copy the string based not only on the number of bytes requested but also on the current offset: if the offset has been shifted, we write from this offset and not from the beginning.
  • Increment the offset by the number of characters sent to the user: *o += n.
  • Return the number of characters sent, and 0 in case of the end of string.

But then, why did we even bother with this offset stuff, instead of just doing a copy_to_user() with the size of our string?

Indeed, if the user ever tries to read the file with a simple cat command, this will try to read a sufficiently large page, and the number of bytes requested will be greater than LEN (here, 10), so we will return only once the 10 bytes requested.

However, two scenarios may arise:

  • The user can very well read the file without going through the cat command, but simply by making a read() system call by reading character by character!
  • The buffer could be much bigger than what cat uses as a minimum page.

Here is the issue that this would cause: if the user asks to read fewer characters than what we have stored, how do we send our entire string to them?

Well, we use the offset loff_t *o for this. Indeed, each time the user reads by a certain size, we will increment this offset by the same size in order to remember that on the next call, we must start from this index and not from the beginning. Thus, the user will be able to chain several read system calls and still recover the entirety of our buffer.

4.4. Let’s test everything!

For reference, our final code looks like the following.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/fs.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ech0 <[email protected]>");
MODULE_DESCRIPTION("Hello World module");

#define LEN 10

static struct miscdevice my_dev;
static char buf[LEN];

static ssize_t hello_write(struct file *f, const char __user *s, size_t n, loff_t *o)
{
	int retval = -EINVAL;

	if (!f || !s)
		return -EFAULT;
	if (n != LEN)
		return -EINVAL;

	retval = copy_from_user(buf, s, LEN);

	if (retval)
		return -EFAULT;

	printk(KERN_INFO "I have successfully written %s in buffer.", buf);

	return LEN;
}

static ssize_t hello_read(struct file *f, char __user *s, size_t n, loff_t *o)
{
	if (!f || !s || !o)
		return -EFAULT;
	if (*o >= LEN)
		return 0;
	if (n > LEN)
		n = LEN;
	if (copy_to_user(s, &buf[*o], n))
		return -EFAULT;

	*o += n;

	return n;
}

struct file_operations my_fops = {
	.read = hello_read,
	.write = hello_write
};

static int __init hello_init(void) {
	int ret;

	printk(KERN_INFO "Hello World !\n");

	my_dev.minor = MISC_DYNAMIC_MINOR;
	my_dev.name = "my_module_misc";
	my_dev.fops = &my_fops;
	ret = misc_register(&my_dev);

	return ret;
}

static void __exit hello_cleanup(void) {
	printk(KERN_INFO "Cleaning up module.\n");
	misc_deregister(&my_dev);
}

module_init(hello_init);
module_exit(hello_cleanup);
Compilation and basic tests

We compile (make), we load the module (insmod), we give the needed writing rights to our misc device (chmod), and we test all that.

make # compilation

sudo insmod my_module.ko # loading the module

sudo chmod o+w /dev/my_module_misc # writing rights

ls -l /dev/my_module_misc # verification

sudo echo -n "1234567890" > /dev/my_module_misc # writing 10 bytes in our buffer

dmesg # verification : [316644.720207] I have successfully written 1234567890 in buffer.

sudo cat /dev/my_module_misc # reading the buffer : 1234567890

As you can see, we were able to write 10 bytes in our buffer with the echo command and read them with cat.

Special case

But since we made the effort of dealing with the scenario of reading the buffer in an unconventional way, we will try to read it 2 by 2 bytes! For that I coded this little C program to use the read system call:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
	char buf[10];
	int fd = open("/dev/my_module_misc", O_RDONLY);
	while (read(fd, buf, 2)) {
		buf[2] = 0;
		printf("%s\n", buf);
	}
}

We test this right away:

$ gcc test.c -o test
$ sudo ./test
12
34
56
78
90

And it’s a success: we managed to read the entire buffer, 2 by 2 bytes by chaining 5 read system calls.

5. Conclusion

Well, it’s already over. You have learnt how to develop a kernel module, load it, and associate a misc device communication interface to it to manage the reading and writing of data between user-land and kernel-land.

Thanks for the reading.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Scroll to Top