Post

Writing a Linux kernel module (4)

This post is an automatic translation from French. You can read the original version here.

C From Scratch Episode 26

And here we go again with a new video from Imil! Today, we add a few features to our prout kernel module, and most importantly we are going to develop a small program that uses our driver alongside it!

Let’s pick up the small module from last time:

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

MODULE_DESCRIPTION("Prout Prout");
MODULE_AUTHOR("CFS/LFS");
MODULE_LICENSE("PPL");

#define DEVNAME "prout"

static int major ;


static struct file_operations fops = {
};

static int
prout_init(void)
{
    printk("coucou le voila\n") ;
    major=register_chrdev(0, DEVNAME, &fops) ;
    if (major<0) {
        printk("nacasse!!\n");
        return major ;
    }
    printk("Major: %d\n", major) ;
    return 0 ;
}


static void
prout_exit(void)
{
    if (major != 0 ) {
        unregister_chrdev(major, DEVNAME ) ;
    }
    printk("napuuuuuuuuuuuuuuuu\n") ;
}

module_init(prout_init);
module_exit(prout_exit);

As previously explained, the file_operations structure (fops) plays an important role: it allows associating operations (syscalls, basically…) with C functions.

For now, this structure is empty. Let’s compile everything and verify that our module works before continuing:

$sudo insmod ./prou.ko

$sudo dmseg | tail
...
[  393.584520] coucou le voila
[  393.584529] Major: 236

$sudo rmmod prout

$sudo dmesg | tail
...
[  501.692786] napuuuuuuuuuuuuuuuu

(I’ll let you refer to the previous articles for compilation)

As we can see, the module loads and unloads correctly. We can now get to the serious stuff!

Implementing the open syscall

Now that we have a working module, we are going to handle the first syscall involved in reading our /dev/prout node. That is, of course, the “open” syscall.

As last time, the steps are simple:

  • write the prout_open() function
  • fill in the module’s file_operations structure to indicate that the open syscall will be handled by the prout_open function.

This last step is very simple; we just need to modify our code like this:

static struct file_operations fops = {
    .open = prout_open
};

The prout_open function cannot of course be arbitrary. It must follow the prototype declared for the file_operations structure, declared in /usr/src/linux/include/linux/fs.h:

struct file_operations {
    [...]
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    [...]

Which gives us:

static int
prout_open( struct inode *, struct file *) ;


static int
prout_open( struct inode *in, struct file *filep )
{
    printk("Owiiii ! ouvre moi !\n" ) ;
    return 0 ;
}

For the lazy ones, you can download the file here

Our module is ready: don’t forget to recompile and create the /dev/prout node, and we can move on to the client!

Writing the client

The program that will use our driver is very simple: it opens the file /dev/prout and closes it immediately:

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main() {

    int fd, rc ;
    fd = open("/dev/prout", O_RDONLY) ;
    if ( fd <0 ) {

        return EXIT_FAILURE ;

    }
    rc = close(fd) ;
    printf("Errno vaut %d \n", errno) ;

    return rc ;
}

We compile this file (prout_client.c) and run it.

$gcc -o prout_client prout_client.c

$./prout_client
Errno vaut 0

$sudo dmesg | tail -1
Owiiii ! ouvre moi !

Our prout_open function was indeed triggered by the “open” syscall.

We can also verify this by looking at the syscalls made by prout_client:

$strace ./prout_client
execve("./prout_client", ["./prout_client"], 0x7ffc88302370 /* 72 vars */) = 0
brk(NULL)                               = 0x55872a96c000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (Aucun fichier ou dossier de ce type)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=159593, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 159593, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb405542000
close(3)                                = 0
openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\00009\2\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=1794232, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb405540000
mmap(NULL, 1807112, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fb405386000
mmap(0x7fb4053a8000, 1323008, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7fb4053a8000
mmap(0x7fb4054eb000, 307200, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x165000) = 0x7fb4054eb000
mmap(0x7fb405536000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1af000) = 0x7fb405536000
mmap(0x7fb40553c000, 13064, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fb40553c000
close(3)                                = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb405384000
arch_prctl(ARCH_SET_FS, 0x7fb405541580) = 0
mprotect(0x7fb405536000, 16384, PROT_READ) = 0
mprotect(0x558729417000, 4096, PROT_READ) = 0
mprotect(0x7fb405598000, 8192, PROT_READ) = 0
munmap(0x7fb405542000, 159593)          = 0
openat(AT_FDCWD, "/dev/prout", O_RDONLY) = 3
close(3)                                = 0
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0
brk(NULL)                               = 0x55872a96c000
brk(0x55872a98d000)                     = 0x55872a98d000
write(1, "Errno vaut 0 \n", 14Errno vaut 0
)         = 14
exit_group(0)                           = ?
+++ exited with 0 +++

If you’re lost, don’t panic: the two lines you care about among all this mess are these:

openat(AT_FDCWD, "/dev/prout", O_RDONLY) = 3
close(3)                                = 0

Everything goes as expected: our program makes two syscalls. The first, “openat”, is handled by our module. The second, “close”, has no function associated with it in our driver’s file_operations structure. We are good citizens, meaning we do close the file, but for now it has no effect. It’s time to fix that!

Now let’s implement the syscall for close:

The approach is exactly the same as before. Our code is now the following:

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

MODULE_DESCRIPTION("Prout Prout");
MODULE_AUTHOR("CFS/LFS");
MODULE_LICENSE("PPL");

#define DEVNAME "prout"


static int
prout_open( struct inode *, struct file *) ;

static int
prout_close( struct inode *, struct file *) ;

static struct file_operations fops = {
    .open = prout_open,
    .release = prout_close
};

static int major ;


static int
prout_init(void)
{
    printk("coucou le voila\n") ;
    major=register_chrdev(0, DEVNAME, &fops) ;
    if (major<0) {
        printk("nacasse!!\n");
        return major ;
    }
    printk("Major: %d\n", major) ;
    return 0 ;
}


static void
prout_exit(void)
{
    if (major != 0 ) {
        unregister_chrdev(major, DEVNAME ) ;
    }
    printk("napuuuuuuuuuuuuuuuu\n") ;
}


static int
prout_open( struct inode *in, struct file *filep )
{
    printk("Owiiii ! ouvre moi !\n" ) ;
    return 0 ;
}

static int
prout_close( struct inode *in, struct file *filep )
{
    printk("Mmmh! ferme bien la porte !\n" ) ;
    return 0 ;
}

module_init(prout_init);
module_exit(prout_exit);

File: prout.c

As hoped, the “close” syscall does indeed trigger our prout_close function:

$./prout_client
Errno vaut 0

$dmesg | tail -4
[ 2859.189223] coucou le voila
[ 2859.189226] Major: 236
[ 2878.249929] Owiiii ! ouvre moi !
[ 2878.249932] Mmmh! ferme bien la porte !

There is not much to say about this implementation, as it is very similar to the prout_open function. Note, however, the totally non-intuitive name of the fops structure field: “release”. Honestly, they could have come up with something simpler…

Our module is now complete! Congratulations!

Addendum

What if we forget to close the file in the client? Is that a problem?

To find out, we can comment out the file closing in prout_client:

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main() {

    int fd, rc ;
    fd = open("/dev/prout", O_RDONLY) ;
    if ( fd <0 ) {

        return EXIT_FAILURE ;

    }
    /* rc = close(fd) ; */
    printf("Errno vaut %d \n", errno) ;

    return rc ;
}

Yet the test shows that the “prout_close” function is still triggered:

$./prout_client
Errno vaut 0

$dmesg | tail -4
[ 6860.181223] coucou le voila
[ 6860.643226] Major: 236
[ 6879.355669] Owiiii ! ouvre moi !
[ 6879.567832] Mmmh! ferme bien la porte !

Is it the libc catching up? That would be conceivable, however the strace command does not show a call to the “close” syscall:

$strace ./prout_client
execve("./prout_client", ["./prout_client"], 0x7ffc9358a7b0 /* 72 vars */) = 0
brk(NULL)                               = 0x561d413d4000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (Aucun fichier ou dossier de ce type)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=159593, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 159593, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9b8845e000
close(3)                                = 0
openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\00009\2\0\0\0\0\0"..., 832) = 832
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=1794232, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9b8845c000
mmap(NULL, 1807112, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9b882a2000
mmap(0x7f9b882c4000, 1323008, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f9b882c4000
mmap(0x7f9b88407000, 307200, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x165000) = 0x7f9b88407000
mmap(0x7f9b88452000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1af000) = 0x7f9b88452000
mmap(0x7f9b88458000, 13064, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9b88458000
close(3)                                = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9b882a0000
arch_prctl(ARCH_SET_FS, 0x7f9b8845d580) = 0
mprotect(0x7f9b88452000, 16384, PROT_READ) = 0
mprotect(0x561d40e6a000, 4096, PROT_READ) = 0
mprotect(0x7f9b884b4000, 8192, PROT_READ) = 0
munmap(0x7f9b8845e000, 159593)          = 0
openat(AT_FDCWD, "/dev/prout", O_RDONLY) = 3
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0
brk(NULL)                               = 0x561d413d4000
brk(0x561d413f5000)                     = 0x561d413f5000
write(1, "Errno vaut 0 \n", 14Errno vaut 0
)         = 14
exit_group(0)                           = ?
+++ exited with 0 +++

The file closing is most likely done by the kernel, during the exit_group syscall.

This hypothesis is further supported by what the man page of said syscall tells us:

Note: glibc provides no wrapper for exit_group(), necessitating the use of syscall(2).

Final words

The journey is now over: this article concludes this series on writing Linux drivers. Imil announced that he is going to start a new arc as part of his Saturday streams, and I can’t wait to write them up here.

On my end, I feel there is still a bit more ground to cover before I fully master this. Ideally, I would like to implement a small homemade driver for a USB gadget. So I’ll see you in the coming weeks to play around a bit more in this direction :)

See you soon,

Rancune

This post is licensed under CC BY 4.0 by the author.