Retrouvez cet article dans : Linux Magazine 105
1. Utilisation basique
Avant de se lancer corps et âme dans l’écriture d’un module d’exemple, je vous propose de nous familiariser avec l’utilisation même des modules noyau. En effet, si ces derniers sont incontournables et très communément utilisés dans le monde GNU/Linux et FreeBSD, ils sont étrangement très peu populaires dans le Landerneau NetBSD. Non qu’ils ne soient pas supportés, loin de là, ou que leur programmation soit d’une complexité insurmontable, mais finalement très peu de parties du noyau trouvent leur version modulaire. Un simple petit tour dans le répertoire$ ls /usr/lkm/ adosfs.o dummy_pci.o kernfs.o procfs.o bsdcomp.o exec_freebsd_aout.o lfs.o ptyfs.o cd9660.o exec_freebsd_elf.o mfs.o smbfs.o coda.o exec_linux_elf.o msdosfs.o tap.o coda5.o exec_pecoff.o ntfs.o tmpfs.o compat_freebsd.o exec_svr4_elf.o nullfs.o udf.o compat_linux.o ext2fs.o overlay.o umapfs.o compat_pecoff.o fdesc.o pf.o union.o compat_svr4.o filecorefs.o portal.o vnd.o deflate.o if_ipl.o powernow.o wi_pcmcia.oShocking, isn’t it ? Préalablement à toute manipulation, nous devons vérifier que notre noyau possède bien le support des modules chargeables. Évidemment, pour poursuivre notre périple, il sera indispensable de posséder les sources du système. Plutôt que de faire dans la finesse, je vous propose de télécharger l’intégralité de l’arbre des sources, ce qui vous permettra de vous plonger encore un peu plus dans l’étude du code source d’un OS du bien. En avant :
# cd /usr # cvs -d anoncvs@anoncvs.fr.netbsd.org:/cvsroot co -rnetbsd-4-0 srcComme le laisse supposer la dernière commande, nous travaillerons sur la version 4.0 de NetBSD, dernière en date. L’article s’appliquera toutefois, à quelques modifications près (que nous mentionnerons), à n’importe quelle version de NetBSD supérieure ou égale à 2.0. Une fois l’arbre téléchargé, nous nous rendons dans le répertoire contenant les configurations et nous vérifions l’existence de l’option LKM
$ grep LKM /usr/src/sys/arch/<architecture>/conf/MON_NOYAU options LKM # loadable kernel modulesoù
modload(8)pour charger un module ;modstat(8)fournit des informations sur les modules chargés ;modunload(8)pour décharger un module.
root@obana:~$ pfctl -sm pfctl: /dev/pf: Device not configured root@obana:~$ modload /usr/lkm/pf.o Module loaded as ID 0 root@obana:~$ modstat Type Id Offset Loadaddr Size Info Rev Module Name DEV 0 -1/161 cbdb0000 009c cbdd27c0 2 pf root@obana:~$ pfctl -sm No ALTQ support in kernel ALTQ related functions disabled states hard limit 10000 src-nodes hard limit 10000 frags hard limit 5000 root@obana:~$ modunload pf root@obana:~$ modstat Type Id Offset Loadaddr Size Info Rev Module Name root@obana:~$Parfait, tout semble fonctionner à merveille. Mais quid du chargement automatique d’un module lors de l’allumage de notre machine ? Tout est prévu. Pour activer ce chargement automatique, il est tout d’abord nécessaire de renseigner la valeur suivante dans le fichier
lkm=YES1. Pour de plus amples informations sur l’utilisation des divers types de macros de gestion de listes chaînées (simples, doubles, circulaires…), reportez-vous à la page de manuel
/usr/lkm/pf.o - - - - AFTERMOUNTqui signifie : 1) Charger le module
2. Bon, ça commence ?
Il va de soi que pour entrer dans le vif du sujet, vous aurez besoin, au minimum, d’avoir installé le setint
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmload, blah_lkmunload, blah_lkmstat);
}
Évidemment, nous devons nous arrêter un instant sur cet amas tombé de nulle part. Jetons d’abord un coup d’œil aux arguments passés à cette fonction.
lkm_table, comme son nom l’indique, n’est autre qu’une liste chaînée de type TAILQ1 contenant l’ensemble des modules chargés, ainsi que les informations relatives à ces modules. La structure est également visible dans le header/usr/include/sys/lkm.h.cmd, on peut s’en douter, est un entier décrivant une commande :LKM_E_LOAD,LKM_E_UNLOADouLKM_E_STAT.ver, quant à lui, permet de vérifier que le versionning de l’environnement de compilation du module correspond bien au noyau auquel il essaye de se lier.- La macro
DISPATCHdistribue les appels, fonction de la commande invoquée, à : blah_lkmload()dans le cas d’une commandeLKM_E_LOAD;blah_lkmunload()dans le cas d’une commandeLKM_E_UNLOAD;blah_lkmstat()dans le cas d’une commandeLKM_E_STAT.
MOD_SYSCALL, pour l’écriture d’un module implémentant un appel système ;MOD_VFS, si l’on souhaite implémenter un nouveau type de système de fichier virtuel ;MOD_DEV, probablement l’un des plus utilisés, vise l’écriture de drivers de typeblockoucharacter;MOD_COMPAT, pour le support de systèmes d’exploitation étrangers ;MOD_EXEC, pour l’écriture d’un nouveau type de binaire ;MOD_MISC, " Miscellaneous modules ", pour faire à peu près ce que l’on veut, ce sera la macro de choix pour notre premier exemple.
MOD_MISC("blah");
Pour réaliser une première version de notre LKM, nous allons utiliser la fonction imil@obana:~/src/lkm$ cat lkminit_blah.c
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/lkm.h>
int blah_lkmentry(struct lkm_table *, int , int );
MOD_MISC("blah");
int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, lkm_nofunc, lkm_nofunc, lkm_nofunc);
}
Reste bien sûr à écrire le Makefile associé, et, là encore, NetBSD va grandement nous simplifier la tâche. Grâce à l’important travail d’abstraction réalisé par les divers projets BSD, notre fichier Makefile se résume à :
imil@obana:~/src/lkm$ cat Makefile S!= cd /usr/src/sys;pwd KMOD= blah SRCS= lkminit_blah.c NOMAN= # defined WARNS= 1 .include <bsd.kmod.mk>Explication dans le texte :
- L’include directory est évalué à partir d’une suite de commandes shell.
- Le nom du module généré sera
blah. - Le code source se résume dans l’immédiat au fichier
lkminit_blah.c(ce formalisme est nécessaire au bon fonctionnement du framework de compilation des LKM). - Nous ne génèrerons pas de manpage.
- Lorsque WARNS vaut la valeur 1, on passe à gcc les arguments
-Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith -Wno-sign-compare -Wno-traditional(voir/usr/share/mk/bsd.sys.mkpour la liste des valeurs possibles et leur implication).
imil@obana:~/src/lkm$ sudo modload ./blah.o Module loaded as ID 0 imil@obana:~/src/lkm$ sudo modstat Type Id Offset Loadaddr Size Info Rev Module Name MISC 0 - cb327000 0004 cb3270bc 2 blah imil@obana:~/src/lkm$ sudo modunload blah imil@obana:~/src/lkm$ sudo modstat Type Id Offset Loadaddr Size Info Rev Module Name imil@obana:~/src/lkm$Victoire !
3. Un peu d’activité
Nous sommes donc en mesure de compiler, charger et décharger un module qui ne fait rien. Il est maintenant grand temps de créer des gestionnaires dignes de ce nom (ou presque) afin de mettre en place un véritable module qui " faitdestrucs ". Ce que nous nous proposons d’écrire est un bête LKM qui écrira quelque chose à son chargement, à son déchargement et à la réception de la commande stat. Il est de bon ton de vérifier l’existence d’un module afin de ne pas le charger plusieurs fois grâce à la fonctionif (lkmexists(lkmtp)) return EEXIST; /* l’exit code EEXIST est defini dans sys/errno.h */Voici à quoi ressemblent nos gestionnaires :
static int
blah_lkmload(struct lkm_table *lkmtp, int cmd)
{
if (lkmexists(lkmtp))
return EEXIST;
printf("blah: chulaaaaaa\n");
return 0;
}
static int
blah_lkmunload(struct lkm_table *lkmtp, int cmd)
{
printf("blah: chupulaaaaaa\n");
return 0;
}
static int
blah_lkmstat(struct lkm_table *lkmtp, int cmd)
{
printf(“blah: eeeh, tu m\’chatouilles\n”);
return 0;
}
Remplaçons maintenant les appels à int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmload, blah_lkmunload, blah_lkmstat);
}
Compilons, puis manipulons notre LKM :
imil@obana:~/src/lkm$ make # compile lkm/lkminit_blah.o cc -O2 -ffreestanding -fno-strict-aliasing -Wno-pointer-sign -Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith -Wno-sign-compare -Wno-traditional -Werror -nostdinc -I. -I/home/imil/src/lkm -isystem /usr/src/sys -isystem /usr/src/sys/arch -isystem /usr/src/sys/../common/include -D_KERNEL -D_LKM -c lkminit_blah.c # link lkm/blah.o ld -r -o tmp.o lkminit_blah.o mv tmp.o blah.o imil@obana:~/src/lkm$ sudo modload ./blah.o Module loaded as ID 0 imil@obana:~/src/lkm$ dmesg|tail -1 blah: chulaaaaaa imil@obana:~/src/lkm$ sudo modstat Type Id Offset Loadaddr Size Info Rev Module Name MISC 0 - cb327000 0004 cb327158 2 blah imil@obana:~/src/lkm$ dmesg|tail -1 blah: eeeh, tu m’chatouilles imil@obana:~/src/lkm$ sudo modunload blah imil@obana:~/src/lkm$ dmesg|tail -1 blah: chupulaaaaaaC’est quand même la sacrée classe ce
static int
blah_lkmhandle(struct lkm_table *lkmtp, int cmd)
{
int err = 0; /* valeur par defaut, pas d’erreur */
switch(cmd) {
case LKM_E_LOAD:
if (lkmexists(lkmtp))
return EEXIST;
printf("blah: chulaaaaaa\n");
return 0;
case LKM_E_UNLOAD:
printf("blah: chupulaaaaaa\n");
return 0;
case LKM_E_STAT:
printf(„blah: eeeh, tu m\‘chatouilles\n“);
return 0;
default:
err = EINVAL; /* Argument invalide, valeur spécifiée dans sys/errno.h */
break;
}
return err;
}
Il suffit maintenant de retoucher l’appel à int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmhandle, blah_lkmhandle, blah_lkmhandle);
}
Beaucoup plus sexy.
4. Je-t’chatte, tu-t’chattes
Après cette petite mise en bouche, il est temps de s’attaquer aux choses sérieuses. Déblayons le terrain afin de préparer l’écriture de notre Nirvana, le Device Driver. Que souhaitons nous faire à l’aide de ce dernier ? Rien de bien méchant, voici le comportement visé :$ echo pwet > /dev/blah $ dmesg|tail -1 blah: eeeh, j’ai reçu pwet $ cat /dev/blah pwet pwet pwet [...]Cette interaction suppose que :
- Notre device doit pouvoir être lu.
- On doit pouvoir écrire dans notre device.
struct cdevsw {
int (*d_open)(dev_t, int, int, struct lwp *);
int (*d_close)(dev_t, int, int, struct lwp *);
int (*d_read)(dev_t, struct uio *, int);
int (*d_write)(dev_t, struct uio *, int);
int (*d_ioctl)(dev_t, u_long, caddr_t, int, struct lwp *);
void (*d_stop)(struct tty *, int);
struct tty * (*d_tty)(dev_t);
int (*d_poll)(dev_t, int, struct lwp *);
paddr_t (*d_mmap)(dev_t, off_t, int);
int (*d_kqfilter)(dev_t, struct knote *);
int d_type;
};
Plus bas dans le fichier, nous voyons comment annuler certaines opérations :
#define noopen ((dev_type_open((*)))enodev) #define noclose ((dev_type_close((*)))enodev) #define noread ((dev_type_read((*)))enodev) #define nowrite ((dev_type_write((*)))enodev) #define noioctl ((dev_type_ioctl((*)))enodev) #define nostop ((dev_type_stop((*)))enodev) #define notty NULL #define nopoll seltrue #define nommap ((dev_type_mmap((*)))enodev) #define nodump ((dev_type_dump((*)))enodev) #define nosize NULL #define nokqfilter seltrue_kqfilterEnfin, toujours dans sys/conf.h, on trouvera les valeurs applicables au champ d_type :
/* * Types for d_type */ #define D_OTHER 0 #define D_TAPE 1 #define D_DISK 2 #define D_TTY 3Muni de ces informations, nous pouvons écrire la structure de données associée à notre futur device :
static struct cdevsw blah_dev = {
blah_open,
blah_close,
blah_read,
blah_write,
noioctl,
nostop,
notty,
nopoll,
nommap,
nokqfilter,
D_OTHER
};
La dernière étape de la préparation consiste à déclarer le type de module que nous allons écrire. Vous l’aurez certainement deviné, nous allons remplacer la macro name: sous cette appellation incompréhensible, nous devrons indiquer le nom du module, blah dans notre cas.devname: ici nous indiquons le nom du device, nous souhaitons travailler sur/dev/blah, ce sera donc à nouveau blah.bdevp: il ne s’agit pas d’un block device, ce champ auraNULLpour valeur.bdevm: nous plaçons le major à-1, même si ce dernier ne sera pas utilisé.cdevp: ici, nous écrivons l’adresse vers la structure de donnée du device,&blah_dev.cdevm: nous souhaitons que le major du device soit attribué dynamiquement, nous inscrivons -1. Ce qui nous donne :MOD_DEV("blah", "blah", NULL, -1, &blah_dev, -1);Les pré-requis étant maintenant implémentés, nous pouvons passer à l’étape la plus intéressante de notre périple, l’écriture des fonctions associées au driver. Commençons par les plus triviales, blah_open() et blah_close(). Nous n’avons pour ainsi dire besoin de rien de particulier dans ces fonctions, mais pour la beauté du geste, rendons-les un peu verbeuses.
static int
blah_open(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("OH HAI, PRUCESS %d HAS OPNED MEH\n", l->l_proc->p_pid);
return 0;
}
Comme on peut s’en douter, static int
blah_close(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("KTHXBY\n");
return 0;
}
J’ai évidemment gardé le meilleur pour la fin, les fonctions - explicitement, le champ
uio_resid; - implicitement, le champ
uio_rw; - implicitement, le champ
iov_basede la sous-structureuio_iov.
struct iovec {
void *iov_base; /* Base address. */
size_t iov_len; /* Length. */
};
/* ... */
enum uio_rw { UIO_READ, UIO_WRITE };
/* ... */
struct uio {
struct iovec *uio_iov; /* pointer to array of iovecs */
int uio_iovcnt; /* number of iovecs in array */
off_t uio_offset; /* offset into file this uio corresponds to */
size_t uio_resid; /* residual i/o count */
enum uio_rw uio_rw; /* see above */
struct vmspace *uio_vmspace;
};
Ainsi, nous comprenons que #include <sys/lwp.h> /* struct lwp */ #include <sys/proc.h> /* nous allons manipuler une variable de type struct proc */ #define MAXLEN 256 int blah_lkmentry(struct lkm_table *, int , int ); static int blah_lkmhandle(struct lkm_table *, int ); /* déclaration de nos fonctions associées */ static int blah_open(dev_t, int, int, struct lwp *); static int blah_close(dev_t, int, int, struct lwp *); static int blah_read(dev_t, struct uio *, int); static int blah_write(dev_t, struct uio *, int); static char inside[MAXLEN];Pour faire circuler nos données d’un espace à l’autre, nous utiliserons la fonction
/* déplacement de inside vers uio pour présentation à l’espace utilisateur */
static int
blah_read(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, n, uio);
}
return error;
}
Nous sommes ici dans une opération de lecture. Aussi, le champ uio_rw de la structure uio sera initialisé avec la valeur /* déplacement de uio vers inside pour copie dans l’espace noyau */
static int
blah_write(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
memset(inside, 0, MAXLEN);
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, uio->uio_resid, uio);
printf(“blah: eeeh, j’ai recu %s”, inside);
}
return error;
}
Avant d’écrire quoi que ce soit dans la variable interne, #include <sys/param.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/lkm.h>
#include <sys/lwp.h>
#include <sys/proc.h>
#include <sys/errno.h>
#define MAXLEN 256
int blah_lkmentry(struct lkm_table *, int , int );
static int blah_lkmhandle(struct lkm_table *, int );
static int blah_open(dev_t, int, int, struct lwp *);
static int blah_close(dev_t, int, int, struct lwp *);
static int blah_read(dev_t, struct uio *, int);
static int blah_write(dev_t, struct uio *, int);
static char inside[MAXLEN];
static struct cdevsw blah_dev = {
blah_open,
blah_close,
blah_read,
blah_write,
noioctl,
nostop,
notty,
nopoll,
nommap,
nokqfilter,
D_OTHER
};
MOD_DEV("blah", "blah", NULL, -1, &blah_dev, -1);
static int
blah_open(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("OH HAI, PRUCESS %d HAS OPNED MEH\n", l->l_proc->p_pid);
return 0;
}
static int
blah_close(dev_t dev, int flag, int mode, struct lwp *l)
{
printf("KTHXBY\n");
return 0;
}
/* copy from kernel (inside) to userland (uio) */
static int
blah_read(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, n, uio);
}
return error;
}
/* copy from userland (uio) to kernel (inside) */
static int
blah_write(dev_t dev, struct uio *uio, int ioflag)
{
int n, error = 0;
memset(inside, 0, MAXLEN);
while (uio->uio_resid > 0 && error == 0) {
n = min(MAXLEN, uio->uio_resid);
error = uiomove(inside, uio->uio_resid, uio);
printf(“blah: eeeh, j’ai recu %s”, inside);
}
return error;
}
static int
blah_lkmhandle(struct lkm_table *lkmtp, int cmd)
{
int err = 0;
switch(cmd) {
case LKM_E_LOAD:
if (lkmexists(lkmtp))
return EEXIST;
strncpy(inside, "I IZ TEH INIT\n", MAXLEN - 1);
printf("blah: chulaaaaaa\n");
return 0;
case LKM_E_UNLOAD:
printf("blah: chupulaaaaaa\n");
return 0;
case LKM_E_STAT:
printf(„blah: eeeh, tu m\‘chatouilles\n“);
return 0;
default:
err = EINVAL; /* Argument invalide, valeur spécifiée dans sys/errno.h */
break;
}
return err;
}
int
blah_lkmentry(struct lkm_table *lkmtp, int cmd, int ver)
{
DISPATCH(lkmtp, cmd, ver, blah_lkmhandle, blah_lkmhandle, blah_lkmhandle);
}
Et maintenant ? C’est l’heure du test final bien sûr !
Vous l’aurez noté, l’utilitaire imil@obana:~/src/lkm$ cat postblah.sh #!/bin/sh rm -f /dev/blah mknod /dev/blah c $3 0 chmod 666 /dev/blah exit 0Il suffit alors d’invoquer modload(8) de la sorte :
sudo modload -p ./postblah.sh ./blah.oet de constater :
imil@obana:~/src/lkm$ ls -l /dev/blah crw-rw-rw- 1 root wheel 188, 0 Mar 11 00:08 /dev/blahEnfin, le moment de vérité :
imil@obana:~/src/lkm$ echo "HAI, YOU IZ TEH LIVE ?" > /dev/blah imil@obana:~/src/lkm$ dmesg|tail -3 OH HAI, PRUCESS 10698 HAS OPNED MEH blah: eeeh, j’ai recu HAI, YOU IZ TEH LIVE ? KTHXBY imil@obana:~/src/lkm$ cat /dev/blah HAI, YOU IZ TEH LIVE ? HAI, YOU IZ TEH LIVE ? HAI, YOU IZ TEH LIVE ? HAI, YOU IZ TEH LIVE ? ^CEt soudain, un sentiment de toute-puissance parcourt votre échine.
5. Ouuuh, j’me ferais bien un driver wifi moi
À travers ce minuscule exemple, c’est tout l’univers des mystérieux pilotes du noyau qui s’ouvre à vous. Bien des contrées restent à explorer, et bien des astuces manquent à l’appel, mais, d’ores et déjà, munis de ces quelques lignes, vous êtes en mesure de réaliser moult prouesses, des plus dignes aux plus obscures. Dans la lignée de cette introduction, je vous invite à vous pencher sur les différents types de macros MOD_*, mais, aussi et surtout, à parcourir l’arbre des sources du système NetBSD, source infinie d’inspiration. Notez, pour finir, que les exemples présents dans cet article s’adapteront sans peine au monde OpenBSD, le framework LKM ayant finalement peu évolué entre les deux OS. Liens- FAQ sur la programmation noyau NetBSD : http://www.netbsd.org/fr/Documentation/kernel/programming.html
- Le guide de programmation de pilotes pour NetBSD : http://www.netbsd.org/docs/kernel/ddwg.html Lolcats : http://icanhascheezburger.com/
Retrouvez cet article dans : Linux Magazine 105





Donnez votre avis
Vous devez avoir ouvert une session pour écrire un commentaire.