Ecrire un module kernel sous Linux (4)

Previous

C From Scratch Episode 26

Et nous voici reparti pour une nouvelle vidéo d’Imil ! Aujourd’hui, nous ajoutons quelques fonctionnalités à notre module kernel prout, et surtout nous allons développer un petit programme utilisant notre driver en parallèle !

Reprenons donc le petit module de la dernière fois :

#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);

Comme expliqué précédemment, la structure file_operations (fops) joue un rôle important : elle permet d’associer des opérations (des syscalls quoi …) à des fonctions en C.

Pour l’instant, cette structure est vide. On compile tout ça et on vérifie que notre module fonctionne avant de continuer :

$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

( Je vous laisse consulter les articles précédents pour la compilation )

Comme on peut le voir, le module se charge et se décharge correctement. On va pouvoir attaquer les choses sérieuses !

Implémentons le syscall open

Maintenant que nous disposons d’un module fonctionnel, nous allons gérer le premier des syscalls impliqués dans la lecture de notre noeud /dev/prout. Il s’agit bien sûr du syscall “open”.

Comme la dernière fois, les étapes sont simples :

Cette dernière étape est très simple, il suffit de modifier notre code ainsi :

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

La fonction prout_open ne peut bien entendu pas être quelconque. Elle doit respecter le prototype déclaré pour la structure file_operations, déclarée dans /usr/src/linux/include/linux/fs.h :

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

Ce qui nous donne :

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 ;
}

Pour les paresseux, vous pouvez télécharger le fichier ici

Notre module est prêt : on oublie pas de recompiler et de créer le noeud /dev/prout et nous pouvons passer au client !

Ecriture du client

Le programme qui va utiliser notre driver est très simple : il ouvre le fichier /dev/prout et le referme aussitôt :

#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 ;
}

Nous compilons ce fichier (prout_client.c) et l’exécutons.

$gcc -o prout_client prout_client.c

$./prout_client
Errno vaut 0

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

Notre fonction prout_open a bien été déclenchée par le syscall “open”.

On peut d’ailleurs le vérifier en visualisant les syscalls effectué par 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 +++

Si vous êtes perdus, pas de panique : les deux lignes qui vous intéressent parmi tout ce bazar sont celles-ci :

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

Tout se passe comme prévu, notre programme effectue bien deux syscalls. Le premier, “openat” est traité par notre module. Le second, “close”, n’a pas de fonction qui lui soit associée dans la structure file_operations de notre driver. Nous sommes des gens biens, c’est à dire que nous fermons bien le fichier, mais cela n’a pour l’instant aucun effet. Il est temps de corriger cela !

Implémentons maintenant le syscall pour close :

La démarche est absolument la même que précédemment. Notre code est maintenant le suivant :

#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);

Fichier : prout.c

Comme espéré, le syscall “close” déclenche bien également notre fonction prout_close :

$./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 !

Il n’y à pas grand chose à dire de cette implémentation, car elle est vraiment très similaire à celle de la fonction prout_open. On notera toutefois le nom totalement pas intuitif du champs de la structure fops : “release”. Franchement, on aurait pu imaginer plus simple …

Notre module est maintenant complet ! Félicitations !

Complément

Et si on oublie de fermer le fichier dans le client ? Est-ce grave ?

Pour s’en assurer, nous pouvons commenter la fermeture du fichier dans 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 ;
}

Le test montre pourtant que la fonction “prout_close” est bien toujours déclenchée :

$./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 !

Est-ce la libc qui rattrape le coup ? Cela serait envisagable, cependant la commande systrace ne montre pas d’appel au syscall “close” :

$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 +++

La fermeture du fichier est vraisemblablement faite par le kernel, lors du syscall exit_group.

Cette hypothèse est d’ailleurs renforcée par ce que nous dit la page de man dudit syscall :

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

Le mot de la fin

Le voyage est maintenant terminé : cet article est la fin de cette série sur l’écriture de drivers Linux. Imil nous a annoncé qu’il allait lancer un nouvel arc dans le cadre de ses streams du samedi, et il me tarde de vous les retranscrire ici.

De mon côté, je me dis cependant qu’il y a encore un petit peu de chemin à faire pour que je maitrise totalement le truc. Idéalement, j’aimerais implémenter un petit driver maison pour un gadget USB. Je vous donne donc rendez vous dans les semaines qui viennent pour jouer encore un peu dans cette voie :)

A bientôt,

Rancune