Mon premier module linux

TuxLe noyau linux, le kernel pour les intimes est un peu un mythe pour tout bidouilleur qui n’y a jamais touché. Seul les hackers les plus expérimentés peuvent prétendre mettre les mains dans un morceau de code aussi critique n’est-ce pas ? En fait, non ! Ce n’est pas si compliqué que ça de jouer un peu avec. Je ne parle pas, bien évidemment, de porter le noyau sur une nouvelle architecture ni de contribuer à des drivers graphiques. Mais créer son premier module, le « Hello world » de la programmation noyau est quelque chose de vraiment accessible.

Mise en place

Pour pouvoir compiler son module, il faut avoir accès aux headers du noyau. Ils sont généralement distribués dans un paquets portant le même nom que le paquet du noyau suffixé de -headers. Dans mon cas, sur archlinux, j’utilise le noyau vanilla qui est fourni par le paquet core/linux et les headers sont fournis par le paquet core/linux-headers. Une fois installés, les headers se trouvent généralement dans le répertoire:

/usr/lib/modules/$(uname -r)/build

Afin de vérifier que l’on arrive à compiler un module minimaliste en utilisant ces sources, nous allons créer le Makefile suivant:

obj-m += monmodule.o

Et oui ! Une simple ligne et aucune cible déclarée. Nous allons également créer un fichier monmodule.c:

#include <linux/module.h>

MODULE_DESCRIPTION("Useless module");
MODULE_AUTHOR("Colin Pitrat");
MODULE_LICENSE("GPL v2");

Ce n’est pas tout à fait un module minimaliste (les macros MODULE_* ne sont pas obligatoires) mais il reste cependant très léger puisqu’il ne contient aucun code utile ! On peut cependant le compiler, l’installer, le charger puis le décharger. La première opération peut être faite sans privilège particulier. Pour les 3 suivantes, il faut être root:

 make -C/lib/modules/$(uname -r)/build M=$PWD modules
 make -C/lib/modules/$(uname -r)/build M=$PWD modules_install
 insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz
 rmmod monmodule

L’inconvénient de n’avoir aucun code, c’est que charger et décharger le module n’a aucun effet ! Mais le fait que les 4 étapes fonctionnent sans erreur est déjà un bon début.

Premier pas

Pour commencer, nous allons faire afficher un message à notre module lorsqu’il est chargé et déchargé. Ce n’est pas vraiment utile, mais ça nous permettra de voir que ce que l’on fait à réellement un effet. Jetons un coup d’œil au code suivant:

#include <linux/module.h>

static int __init mon_init(void)
{
  pr_info("Bonjour: monmodule se charge\n");
  return 0;
}

static void __exit mon_exit(void)
{
  pr_info("Au revoir: monmodule se decharge\n");
}

module_init(mon_init);
module_exit(mon_exit);

MODULE_DESCRIPTION("Useless module");
MODULE_AUTHOR("Colin Pitrat");
MODULE_LICENSE("GPL v2");

La fonction mon_init est déclarée comme retournant un int. C’est le code de retour de l’initialisation. Nous verrons plus tard l’effet qu’il peut avoir. Elle est aussi marquée à l’aide de la macro __init. Cette macro indique que cette fonction ne sert qu’à l’initialisation du module et que son code peut donc être déchargé de la mémoire une fois le module chargé. Il existe une macro similaire pour marquer les données ne servant qu’à l’initialisation et pouvant elle aussi être déchargée ensuite. La macro module_init permet de déclarer que cette méthode est celle permettant d’initialiser le module et devant donc être appelée lorsqu’il est chargé.

La fonction mon_exit elle, retourne void car elle ne peut pas échouer (le déchargement du module peut échouer, mais ça ne sera pas à cause de cette fonction). Elle est marquée à l’aide de la macro __exit qui permet de ne pas inclure cette méthode si le module est compilé dans le noyau. Tous les modules peuvent être compilés aussi bien en module qu’en partie intégrante du noyau, auquel cas ils ne seront jamais déchargés et la fonction exit n’est alors pas utile. La macro module_exit permet de déclarer que cette méthode est celle qui doit être appelé avant de décharger le module.

Dans chaque fonction, nous appelons la méthode pr_info qui ressemble un peu à printf. Elle prend une chaîne de caractères et un nombre variable d’arguments qui seront convertis de manière similaire. L’affichage du message se fait dans les logs noyaux que l’on peut voir à l’aide de la commande dmesg:

# dmesg | tail -n 2
[ 4436.230256] Bonjour: monmodule se charge
[ 4438.959244] Au revoir: monmodule se decharge

Paramètres

Il est possible de passer des paramètres au noyau. Il est très facile de déclarer qu’un module accepte un paramètre, et de l’utiliser dans le code:

#include <linux/module.h>

static int monparam __initdata = 0;
module_param(monparam, int, 0);

static int __init mon_init(void)
{
    pr_info("Bonjour: monmodule se charge\n");
    return monparam;
}

static void __exit mon_exit(void)
{
    pr_info("Au revoir: monmodule se decharge\n");
}

module_init(mon_init);
module_exit(mon_exit);

MODULE_DESCRIPTION("Useless module");
MODULE_AUTHOR("Colin Pitrat");
MODULE_LICENSE("GPL v2");

L’entier monparam est marqué à l’aide de la macro __initdata qui est le pendant pour les variables de la macro __init pour les fonctions. La macro module_param permet de spécifier que le module accepte un paramètre qui s’appelle monparam, de type entier. Le troisième paramètre permet de spécifier si le paramètre peut être accédé et modifié à travers le système de fichier /sys. Ainsi, si l’on met 0644 au lieu de 0, on verra le paramètre dans /sys/module/monmodule/parameters/monparam. À un détail prêt: nous l’avons marqué avec __initdata ce qui signifie qu’il est déchargé après l’initialisation du module. Tenter d’accéder au paramètre à travers ce fichier générerait donc une erreur que l’on verrait apparaître dans dmesg:

[ 5997.116489] BUG: unable to handle kernel paging request at f9ef0000
[ 5997.116515] IP: [<c106c5fe>] param_get_int+0xe/0x30
[ 5997.116540] *pde = 33df6067 *pte = 00000000
[ 5997.116554] Oops: 0000 [#1] PREEMPT SMP
[ 5997.116569] Modules linked in: monmodule(O) mymodule(O) ctr ccm snd_hrtimer snd_seq snd_seq_device ext4 crc16 mbcache jbd2 joydev mousedev uvcvideo videobuf2_vmalloc videobuf2_memops videobuf2_core v4l2_common videodev media psmouse serio_raw snd_hda_codec_realtek snd_hda_codec_generic arc4 atkbd coretemp snd_hda_intel i2c_isch snd_hda_controller kvm_intel eeepc_wmi asus_wmi ath9k libps2 snd_hda_codec snd_hwdep kvm snd_pcm ath9k_common ath9k_hw ath mac80211 cfg80211 gpio_sch snd_timer snd atl1c pcspkr i8042 soundcore thermal button sparse_keymap wmi led_class rfkill serio evdev mac_hid shpchp lpc_sch battery ac acpi_cpufreq processor sch_fq_codel ip_tables x_tables reiserfs uvesafb sd_mod ata_generic pata_acpi pata_sch libata ehci_pci scsi_mod uhci_hcd ehci_hcd usbcore usb_common gma500_gfx video
[ 5997.116781]  i2c_algo_bit drm_kms_helper drm agpgart i2c_core [last unloaded: monmodule]
[ 5997.116809] CPU: 1 PID: 1583 Comm: cat Tainted: G           O    4.0.7-2-ARCH #1
[ 5997.116821] Hardware name: ASUSTeK Computer INC. 1201HA/1201HA, BIOS 0302    02/05/2010
[ 5997.116832] task: daeecc80 ti: f56d0000 task.ti: f56d0000
[ 5997.116845] EIP: 0060:[<c106c5fe>] EFLAGS: 00010286 CPU: 1
[ 5997.116860] EIP is at param_get_int+0xe/0x30
[ 5997.116871] EAX: dad31000 EBX: f2b71294 ECX: c161fbfc EDX: f9ef0000
[ 5997.116881] ESI: dad31000 EDI: c106c470 EBP: f56d1e88 ESP: f56d1e78
[ 5997.116891]  DS: 007b ES: 007b FS: 00d8 GS: 00e0 SS: 0068
[ 5997.116902] CR0: 8005003b CR2: f9ef0000 CR3: 357da000 CR4: 000007d0
[ 5997.116910] Stack:
[ 5997.116917]  d4ada9c0 000100ff 00000000 f2b71294 f56d1e98 c106cb20 f2b71294 c106caf0
[ 5997.116942]  f56d1ea8 c106c489 dad31000 f9ee0048 f56d1ec8 c11ed291 e88ea420 f29d6b80
[ 5997.116967]  c14af6f4 f29d6b80 00000001 f56d1f94 f56d1ed8 c11ebecd e88ea420 00000001
[ 5997.116992] Call Trace:
[ 5997.117014]  [<c106cb20>] param_attr_show+0x30/0x60
[ 5997.117029]  [<c106caf0>] ? param_get_string+0x20/0x20
[ 5997.117043]  [<c106c489>] module_attr_show+0x19/0x30
[ 5997.117059]  [<c11ed291>] sysfs_kf_seq_show+0xb1/0x150
[ 5997.117073]  [<c11ebecd>] kernfs_seq_show+0x1d/0x30
[ 5997.117087]  [<c11a7112>] seq_read+0x92/0x3a0
[ 5997.117103]  [<c11ec68e>] kernfs_fop_read+0xee/0x150
[ 5997.117117]  [<c1186d28>] ? rw_verify_area+0x58/0x140
[ 5997.117130]  [<c11ec5a0>] ? kernfs_fop_write+0x160/0x160
[ 5997.117144]  [<c11873df>] __vfs_read+0x1f/0x70
[ 5997.117157]  [<c118749d>] vfs_read+0x6d/0x120
[ 5997.117171]  [<c11875a7>] SyS_read+0x57/0xc0
[ 5997.117189]  [<c14a1697>] sysenter_do_call+0x12/0x12
[ 5997.117198] Code: 24 08 3d 15 58 c1 c7 44 24 04 00 10 00 00 89 04 24 89 54 24 0c e8 43 b9 1e 00 c9 c3 90 55 89 e5 83 ec 10 3e 8d 74 26 00 8b 52 0c <8b> 12 c7 44 24 08 35 dd 5a c1 c7 44 24 04 00 10 00 00 89 04 24
[ 5997.117339] EIP: [<c106c5fe>] param_get_int+0xe/0x30 SS:ESP 0068:f56d1e78
[ 5997.117359] CR2: 00000000f9ef0000
[ 5997.117372] ---[ end trace 5b7808c0c4dd39f7 ]---

Une fois que ce genre d’erreur s’est produite, le module est « planté »: il ne fonctionne plus, le décharger ne fonctionne généralement pas … un reboot est nécessaire.

Il faut donc absolument utiliser 0 pour un paramètre marqué avec __initdata. Si le paramètre doit pouvoir être modifié, ou même simplement visualisé après le chargement du module, il ne faut pas le marquer avec __initdata et fournir les permissions adéquat à la macro module_parameter.

Tentons maintenant de loader notre module avec diverses valeurs pour notre paramètre:

# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz monparam=-1
insmod: ERROR: could not insert module /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz: Operation not permitted
# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz monparam=-2
insmod: ERROR: could not insert module /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz: Unknown symbol in module
# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz monparam=-3
insmod: ERROR: could not insert module /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz: Module has wrong symbol version
# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz monparam=-4
insmod: ERROR: could not insert module /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz: Interrupted system call
# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz monparam=-5
insmod: ERROR: could not insert module /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz: Input/output error
# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz monparam=1
# rmmod monmodule
# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz monparam=2
# rmmod monmodule

On voit donc que la valeur de retour de la fonction d’initialisation du module correspond, lorsque c’est un entier négatif, à un code d’erreur. Les valeurs des principaux codes d’erreurs (positifs, il faut retourner -Exxx) sont disponibles dans le fichier /lib/modules/4.0.7-2-ARCH/build/include/uapi/asm-generic/errno-base.h.

Quand la fonction d’init retourne un nombre positif, notre module semble bien se charger et il faut le décharger avec rmmod. Retourner un nombre positif serait donc acceptable si tout va bien ?

[ 1990.497310] Bonjour: monmodule se charge
[ 1990.497326] do_init_module: 'monmodule'->init suspiciously returned 2, it should follow 0/-E convention
do_init_module: loading module anyway...
[ 1990.497346] CPU: 0 PID: 5388 Comm: insmod Tainted: G           O    4.0.7-2-ARCH #1
[ 1990.497354] Hardware name: ASUSTeK Computer INC. 1201HA/1201HA, BIOS 0302    02/05/2010
[ 1990.497361]  c160b907 4bc145fc 00000000 f565de34 c149c8a4 f9ee0000 f565de5c c149c34e
[ 1990.497384]  c157b5f8 c14b1d34 f9ee000c 00000002 c14b1d34 00000001 f565df5c e5619580
[ 1990.497405]  f565df34 c10d2a53 f9ee0048 c161fb48 00000000 c15834d4 f9ee000c f565deac
[ 1990.497426] Call Trace:
[ 1990.497452]  [<c149c8a4>] dump_stack+0x48/0x69
[ 1990.497471]  [<c149c34e>] do_init_module+0x7a/0x198
[ 1990.497490]  [<c10d2a53>] load_module+0x1e03/0x23c0
[ 1990.497552]  [<c115cabf>] ? map_vm_area+0x2f/0x40
[ 1990.497572]  [<c10d311f>] SyS_init_module+0x10f/0x170
[ 1990.497588]  [<c1186530>] ? do_sync_readv_writev+0xa0/0xa0
[ 1990.497602]  [<c11873df>] ? __vfs_read+0x1f/0x70
[ 1990.497654]  [<c14a1697>] sysenter_do_call+0x12/0x12
[ 1991.581445] Au revoir: monmodule se decharge

Cela fonctionne, mais le noyau nous rappelle que c’est une mauvaise pratique: « it should follow 0/-E convention ». Par gentillesse, il accepte de loader le module (« loading module anyway… ») non sans nous afficher une belle stack pour préciser où se situe l’erreur et nous informer au passage que notre noyau est maintenant « tainted » (c’est en fait le cas même lorsque l’on retourne 0: le drapeau « O » pour « Out Of Tree » (TAINT_OOT_MODULE) signifie que c’est un module ‘fait maison’ et non un module standard du noyau).

On préférera donc toujours retourner 0 en cas de succès et un entier négatif en cas d’erreur.

Autre cas d’erreur: on pourrait être tenté de ne pas déclarer la méthode mon_exit. Après tout, elle ne fait rien, on devrait donc bien pouvoir s’en passer ? Et bien non ! Si l’on déclare un module qui a une fonction passée à module_init mais pas de fonction passée à module_exit, on obtient l’erreur suivante au moment de faire le rmmod:

# rmmod monmodule
rmmod: ERROR: could not remove 'monmodule': Device or resource busy
rmmod: ERROR: could not remove module monmodule: Device or resource busy

L’option -f de rmmod permet heureusement de forcer le déchargement du module, mais encore une fois le mieux est de respecter les règles !

Fournir un service

Le but d’un module est de fournir des fonctions. Ces fonctions peuvent soit être utilisées par d’autre modules qui vont donc dépendre du premier, soit implémenter des interfaces. Par exemple, un module implémentant le support d’un système de fichier va appeler la méthode register_filesystem en passant une structure fournissant les informations nécessaires pour que le noyau parvienne à appeler les méthodes du module au moment de monter le filesystem, lire un fichier, faire un ls …

L’élégance de ce modèle réside dans le fait que le module dépend du noyau. Au moment de la compilation, on peut compiler le module seul en utilisant les headers du noyau qui est déjà en train de tourner sur la machine, puis charger le module, comme on l’a fait dans cet article. Pourtant, à l’exécution, c’est bien le noyau qui utilise le module: lorsqu’on accède au système de fichier, on le fait à travers un appel système qui va ensuite appeler le bon module pour effectuer la tache. Bien sûr, le module peut lui aussi utiliser le noyau puisqu’il en dépend. L’appel à la méthode pr_info en est un exemple.

Pour l’instant, nous allons nous contenter de voir comment nous pouvons créer un premier module qui exporte une fonction et appeler cette fonction depuis un second module. Pour ce faire, nous allons créer un deuxième fichier monautremodule.c. Il est possible de compiler les deux modules à l’aide du même Makefile:

obj-m += monmodule.o
obj-m += monautremodule.o

Le fichier monmodule.c est modifié pour fournir une méthode permettant d’afficher la valeur du paramètre. Cette méthode est exportée à l’aide de la macro EXPORT_SYMBOL. On en profite pour enlever la macro __initdata de la variable monparam et changer les permissions dessus:

#include <linux/module.h>

static int monparam = 0;
module_param(monparam, int, 0644);
static int __init mon_init(void)
{
  pr_info("Bonjour: monmodule se charge\n");
  return 0;
}

void afficher_param(void)
{
  pr_info("Mon param vaut %u\n", monparam);
}

static void __exit mon_exit(void)
{
  pr_info("Au revoir: monmodule se decharge\n");
}

module_init(mon_init);
module_exit(mon_exit);

EXPORT_SYMBOL(afficher_param);

MODULE_DESCRIPTION("Useless module");
MODULE_AUTHOR("Colin Pitrat");
MODULE_LICENSE("GPL v2");

Et on appelle cette fonction depuis l’initialisation du second module, dans le fichier monautremodule.c:

#include <linux/module.h>

void afficher_param(void);

static int __init mon_autre_init(void)
{
    pr_info("monautremodule se charge\n");
    afficher_param();
    return 0;
}

static void __exit mon_autre_exit(void)
{
    pr_info("monautremodule se decharge\n");
    afficher_param();
}

module_init(mon_autre_init);
module_exit(mon_autre_exit);

MODULE_DESCRIPTION("Another useless module");
MODULE_AUTHOR("Colin Pitrat");
MODULE_LICENSE("GPL v2");

À noter qu’il faut déclarer que la méthode que l’on désire appeler existe.

Le résultat de ce code est le suivant:

# insmod /lib/modules/4.0.7-2-ARCH/extra/monmodule.ko.gz
# insmod /lib/modules/4.0.7-2-ARCH/extra/monautremodule.ko.gz
# dmesg | tail
[ 7853.088352] monautremodule: Unknown symbol afficher_param (err 0)
[ 8145.512718] Au revoir: monmodule se decharge
[ 8148.987994] Bonjour: monmodule se charge
[ 8216.276023] monautremodule se charge
[ 8216.276036] Mon param vaut 0
[ 8238.463387] Disabling lock debugging due to kernel taint
[ 8564.470600] Au revoir: monmodule se decharge
[ 8568.374472] Bonjour: monmodule se charge
[ 8572.171045] monautremodule se charge
[ 8572.171057] Mon param vaut 0

# cat /sys/module/monmodule/parameters/monparam
0
# echo "42" > /sys/module/monmodule/parameters/monparam
# cat /sys/module/monmodule/parameters/monparam
42

# rmmod monmodule
rmmod: ERROR: Module monmodule is in use by: monautremodule
# rmmod monautremodule
# dmesg | tail
[ 8148.987994] Bonjour: monmodule se charge
[ 8216.276023] monautremodule se charge
[ 8216.276036] Mon param vaut 0
[ 8238.463387] Disabling lock debugging due to kernel taint
[ 8564.470600] Au revoir: monmodule se decharge
[ 8568.374472] Bonjour: monmodule se charge
[ 8572.171045] monautremodule se charge
[ 8572.171057] Mon param vaut 0
[ 8635.472079] monautremodule se decharge
[ 8635.472095] Mon param vaut 42

En guise d’au revoir

Voilà pour ce premier aperçu de la programmation noyau. Comme promis, ce n’était pas bien compliqué. Et pourtant, nous avons vu pas mal de choses … Pas encore de quoi écrire un module utile, mais nous n’en sommes pas si loin.

Dans un prochain article, nous regarderons de plus prêt à quoi ressemble un vrai module et par quel mécanismes il peut fournir un véritable service à l’utilisateur du système d’exploitation (non polluer les logs au chargement et déchargement du module n’est pas un véritable service).

Cette entrée a été publiée dans Informatique, Noyau linux. Vous pouvez la mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

− two = six