2 points par GN⁺ 2024-07-28 | 1 commentaires | Partager sur WhatsApp
  • 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’apprendre module_init(), module_exit(), kbuild, insmod, rmmod et dmesg, 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 .ko avec make, vérifier avec modinfo, charger avec insmod, consulter les journaux avec dmesg ou journalctl -k, puis décharger avec rmmod
  • devtools/ construit les sources du noyau et un système de fichiers racine BusyBox, démarre le tout dans QEMU, partage ensuite examples/ 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() et module_exit() est préféré ; l’ancienne approche init_module() et cleanup_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_user et copy_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 sur put_user et le traitement d’une écriture non prise en charge
  • L’exemple /proc couvre proc_create, proc_ops, les callbacks de lecture/écriture et l’API seq_file, en reflétant le changement introduit après Linux v5.6, où proc_ops a remplacé file_operations pour 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é via IRQ_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 likely et unlikely, 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_MODVERSIONS ne correspondent pas, on obtient Invalid module format ou 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 restricted apparaît, il faut désactiver SecureBoot ou suivre la procédure de signature des modules

Environnement pratique basé sur QEMU

  • devtools/setup.sh télécharge et compile le tarball du noyau et BusyBox, puis empaquette l’initramfs
  • devtools/build-modules.sh compile les modules pour le noyau QEMU cible, devtools/boot.sh fournit un shell invité, et devtools/test-modules.sh exécute des tests automatiques insmod et rmmod pour chaque module
  • Le débogage GDB se fait en compilant vmlinux avec LKMPG_NO_PREBUILT=1 devtools/setup.sh, puis via devtools/boot.sh --gdb et 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_KERNEL ou 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 cdev est recommandée plutôt que register_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_ops dans Linux v5.6, le changement de type de retour de remove() dans Linux 6.11 et les évolutions des helpers blk-mq entre 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_CODE et KERNEL_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() ou kzalloc()
  • 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

 
GN⁺ 2024-07-28
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

    • Il existe quelques articles qui décrivent l’écriture d’un pilote et la création/connexion d’un périphérique personnalisé avec QEMU : [0] https://blog.davidv.dev/posts/learning-pcie/, [1] https://blog.davidv.dev/posts/pcie-driver-dma/
    • Avec virtme-ng https://github.com/arighi/virtme-ng, il devient vraiment facile de démarrer dans QEMU un noyau en cours de développement
    • Pour le débogage de noyau en phase très précoce, sans même console, QEMU est beaucoup utilisé
      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écessaire gdb-multiarch, ainsi que qemu-system-arm sur l’hôte, puis à lancer qemu-system-aarch64 à l’arrêt avec -S -gdb tcp::1234 et à s’y connecter depuis un autre terminal avec gdb-multiarch ./vmlinux
      Ensuite, dans GDB, en exécutant target remote :1234, break __parse_cmdline puis continue, 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 à pas
      Pour 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 avec echo "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinit
    • La suite de tests WireGuard actuellement intégrée au noyau est un bon exemple pour développer un module noyau avec QEMU et aller jusqu’aux tests automatisés
    • Greg KH a dit assez clairement qu’il n’y aurait pas de 4e édition de LDD
  • Threads 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

    • La table des matières a l’air bonne, mais c’est dommage qu’il n’y ait pas de précommande pour soutenir la production
  • 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

    • Même si c’est un peu fastidieux, on pourrait tout simplement faire tourner le compilateur sur un Raspberry Pi, non ?
  • 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 :)