Guide de programmation des modules du noyau Linux
(sysprog21.github.io)- Linux Kernel Module Programming Guide est un guide gratuit pour créer des modules de noyau chargeables sur Linux v5.10 et versions ultérieures, couvrant d’un seul tenant l’environnement de développement, la compilation, le chargement, le débogage et les principales interfaces du noyau
- Les premiers exemples, avec
hello-*.c, permettent d’apprendremodule_init(),module_exit(),kbuild,insmod,rmmodetdmesg, tandis que les devtools basés sur QEMU réduisent le risque d’endommager le système hôte - Les modules du noyau s’exécutent dans l’espace d’adressage du noyau ; ainsi, un pointeur erroné, un mauvais ordre de déchargement, des problèmes de concurrence ou des erreurs de copie de mémoire utilisateur peuvent entraîner une corruption de la mémoire du noyau ou une instabilité du système
- Le guide s’étend aux périphériques caractère, à
/proc, àseq_file, aux IRQ threadées, à l’input, au PCI, à l’USB, au bloc, au réseau, au Device Model, au Device Tree et jusqu’aux static keys, en revenant sans cesse sur l’ordre d’enregistrement et de désenregistrement ainsi que sur la gestion du cycle de vie - Comme les API internes du noyau changent selon les versions, il faut vérifier des conditions comme
LINUX_VERSION_CODE,KERNEL_VERSION,CONFIG_MODVERSIONS, la signature SecureBoot ou encore le version magic, et les exemples incluent aussi de la compilation conditionnelle
Structure du guide et flux de base
- Ce guide est un support d’apprentissage des modules du noyau, disponible avec un dépôt GitHub et un document PDF, et peut être copié, modifié et redistribué selon les termes de l’Open Software License 3.0
- Le guide actuel prend Linux v5.10 comme base minimale prise en charge et vise à maintenir la compatibilité des exemples et des recommandations avec l’ensemble des noyaux à support long terme
- L’apprenant doit connaître le langage C et avoir de l’expérience dans l’écriture de programmes pour processus classiques ; les modules du noyau se chargent et se déchargent dynamiquement afin d’étendre les fonctions du noyau sans redémarrage
- Le flux de développement de base consiste à installer les en-têtes du noyau, compiler le
.koavecmake, vérifier avecmodinfo, charger avecinsmod, consulter les journaux avecdmesgoujournalctl -k, puis décharger avecrmmod devtools/construit les sources du noyau et un système de fichiers racine BusyBox, démarre le tout dans QEMU, partage ensuiteexamples/via 9p virtfs, puis permet de tester les modules dans l’invité- Pour l’initialisation et le nettoyage des modules, l’usage de
module_init()etmodule_exit()est préféré ; l’ancienne approcheinit_module()etcleanup_module()peut provoquer un échec de compilation dans certaines conditions sur les noyaux 6.15 et suivants lorsque x86 IBT est activé - Un module du noyau n’utilise ni
printf()ni la libc et ne peut s’appuyer que sur les symboles exportés par le noyau ; la sortie n’est pas envoyée au terminal, mais au tampon circulaire des journaux du noyau - Le transfert de données entre l’espace utilisateur et l’espace noyau exige des fonctions dédiées comme
put_user,get_user,copy_to_useretcopy_from_user - L’exemple de périphérique caractère montre
register_chrdev,file_operations, un numéro majeur dynamique, la création d’un nœud/dev, l’ouverture exclusive, une lecture basée surput_useret le traitement d’une écriture non prise en charge - L’exemple
/proccouvreproc_create,proc_ops, les callbacks de lecture/écriture et l’APIseq_file, en reflétant le changement introduit après Linux v5.6, oùproc_opsa remplacéfile_operationspour les gestionnaires/proc - Les IRQ threadées utilisent
request_threaded_irq()pour séparer top-half et bottom-half ; le top-half n’effectue qu’un minimum de travail en contexte d’interruption et réveille le bottom-half threadé viaIRQ_WAKE_THREAD - Les chapitres suivants s’étendent à des domaines de pilotes réels comme l’input, le PCI, l’USB, le bloc, le réseau, le Device Model et le Device Tree, en se concentrant sur les modes d’enregistrement de chaque sous-système et sur le choix de l’ABI userspace
- Les parties consacrées à l’optimisation et à la sûreté traitent de
likelyetunlikely, des static keys, de la petite taille de la pile noyau, de l’interdiction d’utiliser le FPU, des fuites dues à un padding non initialisé et des précautions à prendre avec les API internes préfixées par un double underscore
Contraintes rencontrées dès la compilation et le chargement
- Un module compilé pour un noyau donné peut ne pas se charger sur un autre ; si le version magic ou
CONFIG_MODVERSIONSne correspondent pas, on obtientInvalid module formatou un décalage de version de symboles - Les noyaux de la plupart des distributions Linux généralistes peuvent avoir le modversioning activé ; si les exemples ne fonctionnent pas immédiatement, il faut envisager un noyau sans modversioning ou un environnement QEMU
- Sur un système où SecureBoot est activé, le chargement de modules non signés peut être restreint ; si
Lockdown: insmod: unsigned module loading is restrictedapparaît, il faut désactiver SecureBoot ou suivre la procédure de signature des modules
Environnement pratique basé sur QEMU
devtools/setup.shtélécharge et compile le tarball du noyau et BusyBox, puis empaquette l’initramfsdevtools/build-modules.shcompile les modules pour le noyau QEMU cible,devtools/boot.shfournit un shell invité, etdevtools/test-modules.shexécute des tests automatiquesinsmodetrmmodpour chaque module- Le débogage GDB se fait en compilant
vmlinuxavecLKMPG_NO_PREBUILT=1 devtools/setup.sh, puis viadevtools/boot.sh --gdbet une connexion GDB distante
Règles d’écriture du code noyau
- Dans une fonction d’initialisation, un enregistrement ou une allocation peut échouer ; les ressources acquises doivent donc être libérées dans l’ordre inverse dans des chemins d’erreur fondés sur
goto - Lorsqu’on enregistre une structure de callbacks dans le noyau, l’espace utilisateur peut appeler ces callbacks avant même que l’init ne retourne ; il est donc crucial de terminer l’initialisation interne avant l’enregistrement final, puis de désenregistrer d’abord lors du nettoyage, selon la règle register last, unregister first
- Le process context, le softirq/tasklet context et le hardirq context diffèrent quant à la possibilité de dormir, d’accéder à la mémoire utilisateur, d’utiliser
GFP_KERNELou des mutex ; mal comprendre cette distinction mène à des bugs noyau très courants
Points d’attention selon les périphériques et sous-systèmes
- Les périphériques caractère sont identifiés par un numéro majeur pour le pilote et un numéro mineur pour distinguer plusieurs périphériques internes ; dans l’approche moderne, l’interface
cdevest recommandée plutôt queregister_chrdev() - Un pilote PCI ne suppose pas d’adresses fixes ; il mappe les ressources BAR énumérées par le cœur PCI, et dans le code Linux 5.10 et suivants,
pcim_enable_device()et les API de ressources gérées par le périphérique aident à réduire les bugs au démontage - Un pilote USB doit traiter le hotplug et la déconnexion comme des événements normaux, et être conçu en supposant des courses possibles entre l’achèvement d’URB, la déconnexion, le timeout, la suspension et l’arrêt côté espace utilisateur
- Un pilote bloc fonctionne autour de
blk-mq,request,gendisk, des queue limits et de la sémantique flush/FUA, et participe à un modèle de complétion asynchrone des requêtes plutôt qu’à de simples callbacks de lecture/écriture - Un pilote réseau s’articule avec
struct net_device,net_device_ops,sk_buff, NAPI, les indicateurs de fonctionnalités d’offload et le signalement de l’état du lien ; une déclaration d’offload incorrecte peut entraîner une corruption du trafic
S’adapter aux changements de version du noyau
- Les exemples gèrent par compilation conditionnelle le changement de signature de
class_create()dans Linux 6.4,proc_opsdans Linux v5.6, le changement de type de retour deremove()dans Linux 6.11 et les évolutions des helpersblk-mqentre Linux 5.15 et 6.9 - Les interfaces internes du noyau changent plus souvent que les appels système ; un module qui prend en charge plusieurs noyaux peut difficilement éviter les comparaisons entre
LINUX_VERSION_CODEetKERNEL_VERSION
Points de contrôle de sûreté
- La pile noyau est bien plus petite que la pile de l’espace utilisateur, souvent de l’ordre de 8 KiB ou 16 KiB sur de nombreux systèmes ; pour les grands tableaux, il faut utiliser
kmalloc()oukzalloc() - Lorsqu’on envoie des données vers l’espace utilisateur avec
copy_to_user(), tous les octets, y compris ceux du padding, doivent avoir été initialisés, faute de quoi une fuite d’informations de mémoire noyau peut se produire - Les API commençant par un double underscore, comme
__kmalloc()ou__list_add(), peuvent supposer des préconditions internes ; sauf indication contraire dans la documentation, il faut privilégier les wrappers publics
Périmètre non couvert
- Il est indiqué que certains chunks du texte d’origine ont été omis en raison de limites de longueur et de coût lors du traitement ; ce résumé ne couvre donc pas de manière exhaustive tous les chapitres, exemples et chemins de code du guide
1 commentaires
Avis sur Hacker News
QEMU est un bon moyen de s’essayer au hacking du noyau
Ce serait bien que quelqu’un mette à jour LDD (Linux Device Drivers) et les livres sur le noyau Linux ; comme ce type d’ouvrage technique est difficile à rentabiliser, la Linux Foundation pourrait aussi le financer
Cette semaine encore, j’ai reproduit avec QEMU + GDB un problème de la v6.8 où, sur arm64, le noyau s’arrêtait immédiatement et silencieusement si le paramètre de ligne de commande du noyau dépassait 146 caractères ; en émulant un build de noyau arm64 depuis un hôte Debian 12 Bookworm amd64, j’ai suivi le code problématique ligne par ligne jusqu’à en trouver la cause
Le flux consiste à construire l’image de noyau arm64 et les scripts pour GDB dans un environnement disposant des dépendances de build et des outils de cross-compilation, à installer
gdb, si nécessairegdb-multiarch, ainsi queqemu-system-armsur l’hôte, puis à lancerqemu-system-aarch64à l’arrêt avec-S -gdb tcp::1234et à s’y connecter depuis un autre terminal avecgdb-multiarch ./vmlinuxEnsuite, dans GDB, en exécutant
target remote :1234,break __parse_cmdlinepuiscontinue, on peut utiliser les fonctions GDB classiques comme l’inspection de la mémoire, des variables et de la pile, ainsi que l’exécution pas à pasPour le débogage du noyau avec GDB et les scripts
lx-*, voir https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-...Pour que GDB puisse utiliser les scripts Python
lx-*, il faut généralement aussi autoriser le chemin, par exemple avececho "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinitThreads HN liés : https://news.ycombinator.com/item?id=35782630, https://news.ycombinator.com/item?id=28283030
The Linux Memory Manager mérite aussi d’être consulté : https://linuxmemory.org/chapters
D’après la dernière mise à jour envoyée par l’auteur début juillet, le premier jet est terminé et il est maintenant entré en phase d’édition avec l’éditeur
Certains exemples semblent difficiles à exécuter soi-même
Par exemple, “Detecting button presses” part du principe qu’on peut construire un module pour RPi, ce qui peut déjà demander des opérations pas forcément simples, comme de la cross-compilation
C’est excellent : détaillé, axé sur la pratique, et c’est un tutoriel qui fait tout de suite construire des modules noyau
Ressource à lire également : https://0xax.gitbooks.io/linux-insides/content/index.html
Je me demande où consulter de bonnes ressources sur la programmation du noyau Linux en général, comme les systèmes de fichiers ou la gestion mémoire
Il y avait autrefois “Linux Kernel Development” de Robert Love, mais il ne semble plus être mis à jour
La première fois que j’ai lu ça, c’était il y a environ 22 ans :)