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”.
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
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);
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()
andmodule_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
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.
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;
}
A small explanation is needed.
- Function prototype
struct file *f
: file descriptor associated with the deviceconst char __user *s
: user-land pointer of the buffer where the user wrotesize_t n
: number of bytes written by the user in user-landloff_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 nullif (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 anrmmod
) 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;
}
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.
- Checking the
- 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 aread()
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.