Ecrire un module kernel sous Linux (2)

Previous Next

C From Scratch Episode 24 (2/2)

Durant le dernier article, un module kernel simple a été écrit et compilé. Cette fois, nous allons voir comment ce module peut interagir avec l’espace utilisateur via un nœud dans /dev. Ce chapitre suit la structure du stream d’Imil en deux parties : une première qui explique comment on peut utiliser des pointeurs sur fonction dans une structure et une seconde qui explique comment mettra tout cela en musique dans notre module kernel.

Note : J’ai pris la liberté de changer un petit peu les exemples par rapport au stream d’Imil.

Introduction

Sous Unix, de façon générale, on discute avec le matériel au travers d’un device, c’est à dire un pseudo fichier localisé dans /dev. Les outils pour le faire sont ceux que nous connaissons depuis longtemps pour interagir avec des fichiers : cat, echo, etc …

Mais si on y regarde d’un peu plus près, tout cela repose toujours sur des syscalls :

$strace cat /etc/passwd
[...]
read( [...]
[...]
$strace cat /dev/input/mouse0
[...]
read( [...] 
[...]

Ce qui peut surprendre ici, c’est que les deux commandes utilisent le même syscall (read) alors qu’il s’agit de deux objets très différents

C’est de nature tout à fait différente … Et pourtant nous y accédons toujours via le syscall read. Magique non ?

En fait, il faut se rappeler que sous Unix, comme le dit le célèbre adage, tout est fichier. Cela veut dire que nos différents drivers ont chacun leur implémentation de la fonction read(), et que le syscall déclenche la bonne fonction lorsque l’on accède au nœud dans /dev.

Grâce à cette petite astuce, pas besoin de multiplier les syscalls. On utilise l’API qui permet d’accéder à un fichier et puis c’est tout !

Des pointeurs de fonction et des structures

Pour comprendre comme le kernel fait, on peut commencer par le programme suivant :

int dummy_open(int) ;
int dummy_close(int) ;

struct dummy{ 
    int (*open)(int) ;
    int (*close)(int) ;
};

int
main()
{
    int r ;
    struct dummy d = { 
        .open = dummy_open,
        .close = dummy_close
    } ;

    r = d.open(1);
    r = d.close(r) ;
	
    return r ;
}

int
dummy_open( int i )
{ 
    return --i ;
}

int 
dummy_close( int i )
{
    return ++i ;
}

La structure dummy contient deux pointeurs sur fonction, nommés respectivement “open” et “close”. Lors de l’initialisation de la variable d, ces deux pointeurs sont initialisés de façon à pointer respectivement sur dummy_open et dummy_close.

Désormais, lorsque l’on appelle d.open(), c’est en fait dummy_open() qui est appelé. De la même façon, d.close() exécute en fait dummy_close.

Pour bien illustrer la chose, faisons la même chose avec un tableau de deux structures dummy, qui pointent toutes les deux vers des fonctions différentes. Et appelons la fonction open() :

#include <stdio.h>

int dummy_open(int) ;
int dummy_close(int) ;
int yeah_open(int) ;
int yeah_close(int) ;

struct dummy{ 
    int (*open)(int) ;
    int (*close)(int) ;
};

int
main()
{
    struct dummy d[2] ;             // Un tableau de 2 structures dummy
    
    d[0].open = dummy_open ;        // On initialise d[0]
    d[0].close = dummy_close ;

    d[1].open = yeah_open ;         // On initialise d[1]
    d[1].close = yeah_close ;


    d[0].open(1) ;                  // Appel de la fonction open de d[0]
    d[1].open(1) ;                  // Appel de la fonction open de d[1]
	
    return 0;
}

int
dummy_open( int i )
{ 
    printf("dummy_open\n"); 
    return 0 ;
}

int 
dummy_close( int i )
{
    printf("dummy_close\n"); 
    return 0 ;
}

int
yeah_open( int i )
{ 
    printf("yeah_open\n"); 
    return 0 ;
}

int 
yeah_close( int i )
{
    printf("yeah_close\n"); 
    return 0 ;
}

Compilons et testons notre programme :

$gcc -o test main.c

$./test
dummy_open
yeah_open

Comme on peut le voir, c’est la fonction dummy_open() qui est appelée pour d[0], et la fonction yeah_open() pour d[1]. Ça marche !!

Nous allons voir maintenant comment cela est utilisé dans le kernel.

Revenons à notre module !

Il y a, dans le kernel, une structure dont le fonctionnement est très similaire. Elle permet de masquer le fonctionnement d’un device et de n’afficher coté utilisateur que la fonction read via un syscall.

Cette structure se trouve dans les sources du kernel, dans le fichier /usr/src/linux/include/linux/fs.h, et se nomme file_operations. En voici la déclaration :

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
			unsigned int flags);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

Comme on peut le voir, cette structure ne contient que des pointeurs sur fonctions ( on parle de “place-holders” ). Elle fonctionne de la même manière que notre structure dummy.

Pour fabriquer notre driver, on crée le fichier brrr.c :

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>           // Nécessaire pour la structure file_operations
#include <linux/uaccess.h>      // Nécessaire pour la fonction copy_to_user()

MODULE_DESCRIPTION("BRRRRRRRRRRRR");
MODULE_AUTHOR("CFS");
MODULE_LICENSE("BRRRRRRRRRRRRRRRRR");

#define DEVNAME "brrr"

static char brrr[]="brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr\n" ;
static int brrrlen ;
static int major ;

static ssize_t brrr_read( struct file*, char*, size_t, loff_t* ) ;

static struct file_operations fops = {
	.read = brrr_read
};

static int 
brrr_init(void) {
	major = register_chrdev(0, DEVNAME, &fops ) ; 
	if ( major < 0 ) {
		printk("pas content :(\n" ) ;
		return major;
	}
	brrrlen=strlen(brrr) ;
}

static void
brrr_exit(void)
{
	if (major != 0 ) {
		unregister_chrdev( major, DEVNAME ) ;
	}
}

static ssize_t 
brrr_read( struct file* fp, char* buf, size_t len, loff_t* off) 
{
	if ( copy_to_user( buf, brrr, brrrlen ) != 0 ) {
		printk("Oh no !!! \n" ) ;
		return EFAULT ;
	}
	return brrrlen ;
}

module_init(brrr_init);
module_exit(brrr_exit);

La structure du programme est globalement la même que le module de l’article précédent. Cette fois, cependant, le module comporte trois fonctions :

Ce module correspond à un char device. Il s’enregistre auprès du kernel en tant que “brrr” et doit donner son char major. Comme nous n’avons pas de char major attribué, nous laissons le kernel choisir en donnant un 0 à la fonction register_chrdev. Le major qui nous est affecté est renvoyé par la fonction, et stocké dans la variable globale major.

Pour indiquer laquelle de nos fonctions est appelée lors d’un syscall READ, nous lui transmettons également l’adresse de la variable fops, de type file_operations. Cette variable globale est initialisée en haut du code.

C’est finalement la fonction brrr_read qui fait le boulot, grâce à un appel à copy_to_user qui va permettre d’écrire des données en espace utilisateur. Pourquoi ne pas écrire directement sur le pointeur buf ? Et bien tout simplement parce que nous sommes en kernel space, et qu’il faut donc tenir compte de la MMU et de la gestion de mémoire des process !

Pour compiler tout ça, rien de spécial ! On utilise la même méthode que précédemment !

Après avoir vérifié que le module se charge bien en mémoire avec

grep brrr /proc/modules

On peut récupérer son char major :

$sudo grep brrr /proc/devices
509 brrr

Et créer le noeud dans /dev :

$sudo mknod /dev/brrr c 509 0

Il ne reste plus qu’à tester :

$sudo cat /dev/brrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr

Ca maaaaaaaarche ! Merci Imil !!!!!!!!!!!

* S’en va en vibrant :)

Rancune

Biblio: