1. Introduction
Cette première partie explique comment préparer un environnement pour apprendre à programmer des modules et des drivers logiciels Linux.
Le système choisi est Ubuntu dans une machine virtuelle Hyper-V.
Nous utilisons KDevelop pour écrire le code C et Kbuild pour compiler les modules.
Le cours suppose que le langage C est connu, mais n'exige aucune connaissance préalable des API du noyau Linux.
Chaque notion est expliquée avec un exemple, la raison de son utilisation et la façon de vérifier son fonctionnement.
Les essais de modules se font uniquement dans une VM de test avec un point de contrôle. Une erreur dans un driver peut faire planter le noyau.
Sommaire des thèmes
- Principes du noyau Linux et comparaison avec Windows.
- Prérequis matériels et logiciels.
- Installation et configuration d’Hyper-V.
- Préparation d’Ubuntu.
- Environnement KDevelop.
- GCC, Makefile et Kbuild.
- Commandes de gestion des modules.
- Sources et documentation Linux.
- Analyse statique des drivers.
- Débogage et traçage du noyau.
- Compilation croisée et systèmes embarqués.
- TP : création d’un module .ko.
- Exercices, questions pièges et récapitulatif.
CHAPITRE 1 - Architecture et fonctionnement du noyau Linux
2. Rôle du noyau Linux
Le noyau Linux gère les processus, mémoires, systèmes de fichiers, réseaux et périphériques. Ubuntu est une DISTRIBUTION : elle assemble le noyau, les programmes et les outils d'administration.
Exemple : un programme C appelle read(fd, buffer, 128). La libc fournit l'interface, puis le noyau traite l'appel système.
Selon le fichier, il utilise un cache, attend des données ou appelle un pilote. La libc ne pilote pas directement le matériel.
3. User mode et kernel mode
Un processus utilisateur dispose de privilèges limités ; une mauvaise adresse provoque souvent SIGSEGV dans ce processus.
Une erreur en espace noyau peut corrompre l'état global ou causer un oops/panic. Sur x86-64, on parle souvent de ring 3 (utilisateur) et ring 0 (noyau).
Adresse virtuelle et adresse physique sont deux notions distinctes.
4. Qu'est-ce qu'un module ? Un driver ?
Un module noyau chargeable est du code pouvant être ajouté à un noyau en fonctionnement, souvent sous forme de fichier .ko.
Un driver est du code qui fournit une fonctionnalité liée à un périphérique réel ou virtuel ; il peut être intégré directement au noyau ou compilé en module.
Tous les modules ne sont pas des drivers, et tous les drivers ne sont pas des modules.
Notre premier module affichera des messages et ne pilotera aucun matériel.
Ensuite, un character device driver pourra exposer /dev/mon_driver, recevoir read/write et éventuellement ioctl.
Cette progression permet de maîtriser le cycle de vie avant les interruptions et DMA.
5. Comparaison raisonnée avec Windows WDM
DriverEntry rappelle approximativement module_init et DriverUnload rappelle module_exit, mais leurs signatures et modèles diffèrent.
Linux n'a ni IRP WDM ni DEVICE_OBJECT universel. Un character driver peut exposer struct file_operations.
DeviceIoControl et ioctl permettent d'envoyer des commandes spécifiques à un driver, mais les codes, buffers et validations sont différents.
En Linux, il n'existe pas de traduction automatique de METHOD_BUFFERED : les transferts user/kernel doivent respecter les API appropriées.
6. Pourquoi travailler dans une machine virtuelle
Un module erroné peut bloquer Ubuntu, rendre des données incohérentes ou empêcher un redémarrage.
Une VM Hyper-V permet d'isoler les tests du système principal et de revenir à un point de contrôle.
Attention : une VM n'est pas une barrière de sécurité absolue contre toutes les vulnérabilités ; elle constitue surtout un excellent laboratoire pour les crashs accidentels.
Évitez les dossiers hôtes montés en écriture si vous testez du code instable.
7. Kernel Oops et Kernel Panic
Un kernel oops signale un problème grave détecté par le noyau, sans arrêt systématique de toute la machine. Un panic signifie qu'il ne peut pas poursuivre en sécurité.
Les logs (printk/pr_info, dmesg, journal) deviennent donc essentiels : on ne peut pas déboguer comme une application console ordinaire.
Un message de log n'est toutefois pas une preuve de sécurité ni de correction.
À retenir
Notre premier objectif n'est pas de modifier Linux mais de comprendre les conditions nécessaires à une insertion, une exécution et un retrait de module propres.
CHAPITRE 2 - Prérequis matériels et logiciels
8. Configuration recommandée
Pour Ubuntu Desktop avec KDevelop : 2 vCPU, 4 Gio RAM, disque VHDX dynamique de 30 à 40 Gio. Avec de la mémoire disponible, 6 à 8 Gio offrent plus de confort pour l'IDE.
Un Ubuntu Server sans bureau est plus léger. Compiler un petit module consomme bien moins de ressources que reconstruire tout le noyau.
Le VHDX dynamique croît avec les données réellement écrites ; les points de contrôle augmentent aussi l'espace nécessaire sur le disque de l'hôte.
9. Architecture x86-64
Le PC hôte et la VM de ce cours utilisent x86-64. Les headers et les objets d'un module sont construits pour l'architecture et la configuration du noyau cible.
Compiler un module x86-64 ne le rend pas utilisable sur ARM64. De même, le module doit être compatible avec la version, la configuration et l'ABI du noyau cible.
Linux ne garantit pas une ABI stable pour les modules externes.
10. Virtualisation matérielle
Intel VT-x et AMD-V offrent des mécanismes matériels employés par les hyperviseurs.
Sur Windows, le Gestionnaire des tâches > Performances > Processeur peut indiquer l'état de la virtualisation.
Dans les systèmes modernes, le rôle d'Hyper-V et les exigences exactes du poste varient : ne modifiez pas des options UEFI au hasard si Hyper-V fonctionne déjà.
11. Mémoire, CPU et stockage : comprendre les compromis
Un vCPU ralentit parfois GCC ; trop de vCPU retire inutilement des ressources à l'hôte. Les checkpoints consomment du disque.
Les réglages mémoire d'Hyper-V sont distincts des allocations du noyau que nous étudierons.
12. Vérifications de base sous Windows
Dans PowerShell administrateur :
Code: Select all
Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All
Si Hyper-V fonctionne déjà, aucune modification UEFI n'est requise pour commencer le cours.
CHAPITRE 3 - Hyper-V et machine virtuelle Ubuntu
13. Le rôle de l'hyperviseur
Hyper-V fait fonctionner Ubuntu comme système invité (« guest ») sur un hôte Windows (« host »).
Il présente des processeurs virtuels, une mémoire, des disques et des périphériques virtuels.
Pour nos modules logiciels, cet environnement suffit ; l'accès direct à un périphérique physique spécifique n'est pas nécessaire.
14. Créer une VM de génération 2
Dans le Gestionnaire Hyper-V : Nouvelle > Machine virtuelle. Choisissez un nom explicite, par exemple Ubuntu-Kernel-Lab.
Choisissez Génération 2 (démarrage UEFI), réservez environ 4096 Mio de RAM, 2 processeurs virtuels dans les paramètres et créez un disque VHDX d'au moins 30 Gio.
Associez un commutateur virtuel offrant Internet si vous devez installer des paquets.
Attachez l'ISO officielle d'Ubuntu 24.04 LTS au lecteur DVD virtuel, puis démarrez la VM.
15. Secure Boot : deux protections différentes
Dans Hyper-V, le démarrage sécurisé UEFI d'une VM génération 2 peut utiliser le modèle « Microsoft UEFI Certificate Authority » pour Linux.
Il vérifie la chaîne de démarrage. Ubuntu peut également appliquer une politique de signature aux modules noyau tiers.
Il est donc possible qu'Ubuntu démarre correctement, que le fichier .ko compile, MAIS que son insertion échoue avec « Required key not available ».
Ne désactivez pas automatiquement Secure Boot sur votre machine principale.
Pour apprendre dans une VM isolée, vous avez deux choix documentés : signer et faire reconnaître le module (MOK), ou désactiver Secure Boot de CETTE VM si vous acceptez la réduction de protection.
Ce cours ne dépend pas d'une désactivation : le TP explique d'abord comment diagnostiquer le problème.
16. Installation d’Ubuntu dans Hyper-V
Utilisez l'installateur graphique de l'ISO.
L'option effaçant le disque concerne le disque VIRTUEL de la VM, pas le disque Windows hôte, à condition d'avoir correctement attaché le VHDX virtuel.
Vérifiez cette distinction. Créez un utilisateur standard, pas une session permanente root. Une fois Ubuntu démarré, retirez l'ISO du lecteur DVD virtuel si besoin.
17. Réseau et système de fichiers
Un commutateur réseau de type Default Switch (selon l'environnement) peut permettre les mises à jour. Vérifiez une connexion depuis Ubuntu avant d'installer GCC.
N'exposez pas inutilement le service SSH de la VM à Internet.
Pour transférer du code, Git ou une copie de fichiers maîtrisée est préférable à un dossier hôte entièrement accessible en écriture.
18. Points de contrôle : la ceinture de sécurité
Éteignez proprement Ubuntu, puis dans Hyper-V créez un point de contrôle appelé « Ubuntu propre + outils kernel ». Recréez un point avant tout TP risqué.
Le point de contrôle capture un état récupérable de la VM, mais ne remplace pas une sauvegarde indépendante de vos fichiers.
Pour les travaux expérimentaux, évitez de conserver vos seuls exemplaires de code source à l'intérieur de la VM.
19. Premier démarrage : contrôles
Dans un terminal Ubuntu :
Code: Select all
cat /etc/os-release
uname -r
uname -m
free -h
lsblk
Vérifiez notamment x86_64 et notez la release exacte. Ne réutilisez jamais la version écrite dans un livre : les headers devront correspondre à la VM.
Erreur fréquente
un checkpoint n'annule pas nécessairement les écritures effectuées depuis la VM vers un partage hôte externe.
CHAPITRE 4 - Préparation d’Ubuntu
20. Distribution, noyau et en-têtes
Ubuntu livre un noyau construit avec ses propres options, correctifs et conventions de packaging.
Les en-têtes (« headers ») décrivent les interfaces nécessaires à la compilation et s'accompagnent d'autres fichiers générés et de règles Kbuild.
Ils ne représentent pas à eux seuls l'intégralité des sources du noyau.
21. Identifier la version active
Code: Select all
uname -r
ls /lib/modules/$(uname -r)/
readlink -f /lib/modules/$(uname -r)/build
Le dossier /lib/modules/<version>/ contient les modules liés à la version, et le lien « build » doit pointer vers un arbre de compilation préparé pour cette version.
Si readlink échoue ou si le dossier manque, il faudra installer les en-têtes corrects.
22. Mise à jour d’Ubuntu
Code: Select all
sudo apt update
sudo apt full-upgrade
« apt full-upgrade » peut ajouter ou retirer des paquets pour achever une mise à niveau, y compris une mise à jour du noyau.
Lisez les changements proposés avant confirmation. Si le noyau a été mis à jour, redémarrez :
Code: Select all
sudo reboot
Installer des headers pour un noyau récent tout en exécutant l'ancien peut provoquer une confusion évitable.
23. Installation des outils de développement
Après redémarrage :
Code: Select all
sudo apt install build-essential git gdb make pkg-config \
linux-headers-$(uname -r) kmod perl python3
- build-essential : ensemble de dépendances de compilation courantes (dont GCC, make et les outils associés).
- git : récupérer, versionner et comparer du code.
- gdb : debugger surtout nos programmes utilisateurs ; le noyau demande une configuration supplémentaire.
- linux-headers-$(uname -r) : éléments pour construire un module contre le noyau démarré.
- kmod : fournit notamment des commandes de gestion des modules.
- perl : utilisé par divers scripts de l'arbre Linux.
Il ne faut pas remplacer aveuglément les headers par « n'importe quelle version proche ».
24. Vérifier la chaîne de compilation
Code: Select all
gcc --version
make --version
git --version
test -d /lib/modules/$(uname -r)/build && echo "Headers présents"
Elle ne démontre pas que toutes les données nécessaires à Kbuild sont valides ; seule une vraie compilation de module le confirmera. Pour inspecter la liaison :
Code: Select all
ls -ld /lib/modules/$(uname -r)/build
25. Pourquoi ne pas compiler le kernel complet maintenant ?
Compiler un module externe est rapide, ne remplace pas le noyau démarré et permet d'apprendre le cycle de vie.
Reconstruire un noyau complet exige davantage de temps, d'espace disque, de configuration et une stratégie de démarrage de secours.
Nous différons donc cette étape, sans la confondre avec « installer linux-headers ».
26. Travailler sans être root en permanence
Éditez et compilez depuis votre compte normal. Les commandes d'insertion et de retrait demandent des privilèges : « make » sans sudo, puis « sudo insmod » seulement pour tester.
27. Comprendre les erreurs de paquets
« Unable to locate package » : headers ou dépôts incohérents. « No space left on device » : contrôlez df -h.
« Could not get lock » : un autre gestionnaire de paquets fonctionne peut-être ; n'effacez pas un verrou au hasard.
CHAPITRE 5 - Configuration de KDevelop
28. KDevelop : présentation
KDevelop facilite édition, navigation et analyse du C. Il ne remplace pas Kbuild : l'IDE doit lancer make avec les règles et les headers du noyau cible.
29. Installation
Code: Select all
sudo apt install kdevelop clangd clang-format
KDevelop n'exige pas forcément tout l'environnement de bureau KDE : il peut être exécuté sous Ubuntu Desktop avec les bibliothèques requises, installées comme dépendances.
30. Ouvrir un projet de module
Créez un dossier personnel dans ~/kernel-lab/hello_module et ouvrez-le dans KDevelop.
Si l'IDE demande un système de build, utilisez l'import d'un dossier ou d'un projet Makefile lorsque disponible.
Les menus exacts varient selon la version ; le point essentiel est de conserver le Makefile Kbuild et de pouvoir exécuter « make » dans le répertoire source.
31. Ne pas confondre deux familles de headers
Les headers de glibc comme <stdio.h> servent aux applications utilisateur. Les headers noyau comme <linux/module.h> et <linux/init.h> servent au code noyau.
Un module ne doit pas appeler printf(), malloc() ou fopen() de la libc. Il emploie des API noyau distinctes (pr_info, kmalloc, etc. selon le besoin).
32. Indexation et faux diagnostics
Un éditeur peut souligner une macro pourtant compilable lorsque ses chemins d'en-têtes ou ses options de compilation ne reflètent pas Kbuild.
Exemple : MODULE_LICENSE() ou __init sont interprétés avec le contexte de build du noyau, pas celui d'un programme C classique.
Ne prenez jamais « tout est vert dans l'IDE » pour une preuve de bonne compilation ; et inversement, un soulignement rouge ne prouve pas une erreur du driver.
33. Configuration de la commande de build
Dans les réglages du projet, définissez une commande externe qui lance make depuis le dossier du module. Le Makefile du chapitre 12 intégrera le chemin du noyau.
Testez toujours la même commande depuis un terminal : vous distinguerez ainsi un problème de l'IDE d'un problème de build réel.
34. GDB et noyau
GDB sur un exécutable utilisateur peut s'attacher à un processus. GDB ne peut pas simplement « s'attacher » à un module noyau pour obtenir le même débogage.
Il faut un noyau configuré pour le débogage, des symboles exploitables et un moyen adapté de contrôler l'exécution (par exemple kgdb, ou un environnement QEMU avec stub GDB).
Dans notre VM Hyper-V, commençons par les logs, ftrace et l'analyse statique.
35. Style de code
Le noyau privilégie une indentation par tabulations, des noms en minuscules avec underscores et une gestion rigoureuse des retours d'erreur. Adopter ces conventions aide à lire les sources existantes.
CHAPITRE 6 - GCC, Makefiles et Kbuild
36. De test.c à test exécutable
Pour un programme utilisateur :
Code: Select all
cat > test.c <<'C'
#include <stdio.h>
int main(void)
{
printf("Bonjour depuis user mode !\n");
return 0;
}
C
gcc -Wall -Wextra -o test test.c
./test
GCC pilote normalement prétraitement, compilation, assemblage et édition de liens.
Le résultat peut être un exécutable ELF utilisateur, chargé avec l'aide du noyau et de l'éditeur de liens dynamique selon sa construction.
37. Étapes de compilation avec GCC
Code: Select all
gcc -E test.c -o test.i # Prétraitement : #include, #define...
gcc -S test.c -o test.s # Génération assembleur
gcc -c test.c -o test.o # Fichier objet, pas encore exécutable
file test.o
file test
Ce modèle pédagogique simplifie certains détails internes de GCC, mais évite de confondre source, objet et binaire final.
38. Pourquoi ne pas faire « gcc -shared module.c » ?
Un module noyau dépend des options, des headers générés, du traitement de modpost, des symboles exportés, de l'architecture, de la configuration du noyau et des règles de Kbuild.
Un objet ELF ordinaire ne devient pas un module chargeable parce qu'il porte l'extension .ko.
L'édition de liens et les métadonnées sont organisées par le système de build du noyau.
39. Qu'est-ce que Kbuild ?
Kbuild est l'infrastructure de construction du noyau Linux. Il sait quels fichiers et options utiliser pour compiler un noyau ou un module externe.
« Make » est l'outil qui interprète les Makefiles ; « Kbuild » désigne les règles et conventions de compilation propres au noyau.
40. Déclaration d’un module avec obj-m
Considérons un fichier mon_module.c. Dans un fichier Kbuild ou Makefile, la ligne :
Code: Select all
obj-m += mon_module.o
Kbuild produit normalement mon_module.ko après ses étapes supplémentaires.
La ligne NE signifie PAS « ajouter mon_module.o à un projet Visual Studio » ni « créer un programme user-mode ».
Pour un module composé de plusieurs fichiers, on peut utiliser :
Code: Select all
obj-m += capteur_virtuel.o
capteur_virtuel-y := coeur.o commandes.o
Ne nommez pas aussi un fichier source capteur_virtuel.c dans cette organisation si cela crée un conflit avec l'objet composite.
41. Commande de compilation Kbuild
Code: Select all
make -C /lib/modules/$(uname -r)/build M="$PWD" modules
- « make » : moteur qui lit les règles.
- « -C CHEMIN » : change temporairement le répertoire de travail du processus make.
- « $(uname -r) » : expansion de commande du shell, donnant la release active.
- « /build » : chemin préparé pour compiler contre CE noyau.
- « M="$PWD" » : chemin absolu vers le dossier contenant notre module externe.
- « modules » : cible de construction des modules, facultative dans de nombreux cas.
Après une compilation réussie, les fichiers auxiliaires peuvent inclure .mod.c, .mod.o, Module.symvers, modules.order, ainsi que le .ko.
Leur présence exacte dépend de la version et du type de build.
42. Exemple de Makefile complet
Code: Select all
obj-m += mon_module.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Ici « $(shell uname -r) » est évalué par make, tandis que « $(uname -r) » dans une commande shell est évalué par le shell.
La forme « $(MAKE) » transmet les propriétés nécessaires à l'invocation récursive de make.
Les chemins simples ci-dessus conviennent au laboratoire ; des projets complexes doivent gérer les chemins avec espaces et builds séparés.
43. Versioning, vermagic et symboles
Les modules externes ne disposent pas d'une ABI stable entre versions du kernel.
L'information « vermagic » incorporée dans un module aide à vérifier sa compatibilité avec le noyau cible.
Des contrôles de symboles et, selon la configuration, de versions de symboles interviennent aussi.
Un .ko construit pour 6.x.Y ne doit pas être supposé compatible avec n'importe quel autre 6.x.Z.
44. Pourquoi les fichiers .h ne suffisent pas
La construction requiert également configuration et fichiers générés, voire des informations de versions de symboles.
Pour les modules externes, « make modules_prepare » prépare certains éléments d'un arbre source, mais ne crée pas à lui seul Module.symvers nécessaire à certains scénarios avec CONFIG_MODVERSIONS.
Voilà pourquoi installer le paquet Ubuntu de headers adapté est plus sûr pour débuter.
45. Nettoyer les artefacts
Code: Select all
make clean
Ne lancez jamais une commande de nettoyage sur un répertoire sans comprendre où elle pointe. Le futur TP la vérifiera concrètement.
Réflexe
Compiler avec Kbuild contre le noyau réellement démarré, pas avec une ligne gcc improvisée.
CHAPITRE 7 - Commandes de gestion des modules Linux
46. uname : identifier le noyau
Code: Select all
uname -r # Release exacte
uname -m # Architecture
uname -a # Plusieurs informations
Ne construisez pas un module contre des headers dépourvus du suffixe attendu.
47. lsmod : modules actuellement chargés
Code: Select all
lsmod
« Used by » aide à comprendre les dépendances et références, mais sa valeur ne résume pas tous les états possibles d'un module.
Un module présent ne peut pas nécessairement être retiré immédiatement. Sous le capot, lsmod consulte l'état des modules exposé par le noyau, notamment via /proc/modules.
48. modinfo : examiner les métadonnées d'un module
Code: Select all
modinfo -F filename loop
modinfo -F license loop
Code: Select all
modinfo ./mon_module.ko
Certains modules sont intégrés statiquement au noyau et n'apparaissent pas comme .ko classiques.
49. insmod : charger un fichier .ko explicite
Code: Select all
sudo insmod ./mon_module.ko
Il ne résout généralement pas automatiquement toutes les dépendances comme le fait modprobe à partir des bases de modules installés.
D'où l'intérêt de l'utiliser seulement avec nos modules très simples et de lire les messages en cas d'échec.
50. rmmod : retirer un module
Code: Select all
sudo rmmod mon_module
Un callback de sortie mal conçu peut aussi provoquer un problème lors du retrait.
La fonction de sortie doit libérer seulement les ressources effectivement acquises et encore détenues.
51. modprobe : trouver modules et dépendances
Code: Select all
modprobe --show-depends loop
Il est conçu pour prendre en charge les dépendances connues. Un .ko uniquement présent dans ~/kernel-lab/ n'est pas automatiquement indexé comme un module système.
N'installez pas le module expérimental globalement sans nécessité.
52. dmesg : examiner les messages du noyau
Code: Select all
sudo dmesg | tail -n 30
sudo dmesg -w
Un Ubuntu configuré de manière restrictive peut interdire dmesg à un utilisateur non privilégié ; d'où sudo.
Évitez de rendre le journal noyau librement accessible pour résoudre ce seul problème.
53. journalctl -k
Code: Select all
sudo journalctl -k -b --no-pager | tail -n 30
La disponibilité des anciens journaux dépend des réglages de conservation et de persistance. « dmesg » et « journalctl » ne sont donc pas strictement interchangeables.
54. Commandes de navigation dans les sources
Code: Select all
pwd
ls -la
find . -maxdepth 2 -name '*.c'
grep -R -n 'MODULE_LICENSE' .
- pwd affiche le répertoire courant.
- ls -la révèle aussi les fichiers cachés.
- find parcourt les dossiers répondant au critère.
- grep -R -n recherche du texte et indique les numéros de ligne.
55. Identifier un binaire ELF
Code: Select all
file mon_module.ko
readelf -h mon_module.ko
readelf -S mon_module.ko
Un module Linux reste une forme d'objet ELF, mais sa structure et son mode de chargement ne sont pas ceux d'une application classique.
56. Quand utiliser sudo ?
Préférez « sudo insmod » à un terminal root permanent. « sudo make » est inutile pour compiler dans votre dossier personnel et risque de produire des fichiers appartenant à root.
Exercice rapide
Relevez la version du noyau, trois modules chargés et les métadonnées d'un module. Expliquez la différence entre lsmod et modinfo.
CHAPITRE 8 - Documentation et sources du noyau
57. Pourquoi lire les sources ?
Les API internes du noyau évoluent. Les sources et la documentation de la version cible priment sur un tutoriel ancien. Regardez aussi le contexte autorisé et la durée de vie des objets manipulés.
58. Organisation approximative des sources
- drivers/ : pilotes ; kernel/ : mécanismes transversaux ; mm/ : mémoire.
- fs/ : systèmes de fichiers ; net/ : réseau ; arch/ : architectures.
- include/ : déclarations ; Documentation/ : guides ; scripts/ : outils.
59. Téléchargement des sources du noyau
Code: Select all
mkdir -p ~/kernel-lab
cd ~/kernel-lab
git clone --depth 1 https://github.com/torvalds/linux.git linux-mainline
Utilisez les sources publiques du noyau principal pour explorer des concepts ; pour analyser un bogue spécifique à Ubuntu, vérifiez les sources et correctifs de la version Ubuntu concernée.
Un clone complet peut demander beaucoup de disque ; « --depth 1 » ne conserve qu'un historique minimal.
60. Chercher une définition
Code: Select all
cd ~/kernel-lab/linux-mainline
grep -R -n 'struct file_operations' include/linux | head
Chercher du texte n'est pas comprendre une définition : vérifiez le bon fichier et le contexte, puis lisez les commentaires et appels associés.
Pour une API, recherchez aussi ses utilisateurs existants. Les exemples du noyau lui-même constituent de bons modèles, sous réserve de versions compatibles.
61. Documentation avec man
Code: Select all
sudo apt install man-db manpages manpages-dev
man 2 open
man 2 ioctl
man 3 printf
« man 2 ioctl » ne documente PAS à lui seul le callback unlocked_ioctl d'un module Linux.
La même chaîne de lettres peut apparaître de part et d'autre de la frontière user/kernel, avec des signatures différentes.
62. Documentation officielle en ligne
La documentation versionnée se consulte notamment sur https://docs.kernel.org/ . Pour les modules externes, la section kbuild/modules.html est fondamentale.
Pour vérifier un comportement, sélectionnez si possible la version de documentation correspondant à votre noyau.
Les pages « latest » peuvent décrire des fonctions indisponibles sur Ubuntu 24.04 avec son noyau installé.
63. Documentation générée depuis le code
Les commentaires structurés de certaines fonctions et structures alimentent kernel-doc et Sphinx. La couverture est inégale : lisez aussi les déclarations, le contexte et les appelants.
64. Explorer avec un éditeur et des outils
KDevelop, grep, ripgrep, ctags et cscope permettent de retrouver déclarations et usages. Installer ripgrep : « sudo apt install ripgrep ».
Exemple dans l'arbre des sources : « rg -n 'module_init\(' --glob '*.c' . | head ». Vérifiez ensuite les chemins d'erreur des fonctions trouvées.
65. Un piège fréquent : les exemples datés
Les tutoriels anciens peuvent utiliser des interfaces abandonnées. Le noyau ne promet pas une ABI interne stable aux modules externes.
Vérifiez toujours la release cible, la signature de l'API et les recommandations du sous-système.
66. Exemple : lire une déclaration utile
Code: Select all
#include <linux/module.h>
#include <linux/init.h>
Inclure un header ne charge pas un module : le chargement intervient lors de l'insertion du .ko par le noyau.
CHAPITRE 9 - Analyse statique des drivers
67. Pourquoi un simple « make » ne suffit-il pas ?
Compiler un .ko ne prouve ni absence de race condition, ni sécurité mémoire. L'analyse statique complète les warnings du compilateur, la relecture et les tests ; elle ne les remplace pas.
68. checkpatch.pl : style et défauts fréquents
Le script du noyau « scripts/checkpatch.pl » signale notamment des problèmes de convention et de nombreux motifs suspects. Dans l'arbre source Linux obtenu au chapitre 8 :
Code: Select all
cd ~/kernel-lab/linux-mainline
./scripts/checkpatch.pl --no-tree --file \
~/kernel-lab/hello_module/mon_module.c
« --file » examine le fichier C directement ; « --no-tree » évite l'exigence d'un arbre noyau complet dans certains usages. Lisez les messages un par un.
Un avertissement de style ne signifie pas « faille exploitable » ; une absence d'avertissement ne signifie pas « code correct ».
69. Sparse : vérification supplémentaire des annotations
Sparse analyse du code C du noyau et peut exploiter des annotations comme __user pour distinguer certains espaces d'adressage. Installation :
Code: Select all
sudo apt install sparse
Code: Select all
cd ~/kernel-lab/hello_module
make -C /lib/modules/$(uname -r)/build M="$PWD" C=1 modules
Pour une réanalyse plus large, C=2 peut être utile dans certains contextes. Les diagnostics peuvent provenir à la fois du compilateur et de Sparse.
70. Exemple d'erreur conceptuelle de mémoire utilisateur
Le code suivant est volontairement FAUX pour un driver :
Code: Select all
/* NE PAS UTILISER : pointeur utilisateur déréférencé directement */
void mauvais_exemple(const char __user *p)
{
char c = *p;
(void)c;
}
Elle peut être invalide, non accessible à cet instant, ou provoquer une faute ; un kernel driver doit utiliser les mécanismes appropriés (par exemple copy_from_user dans un contexte autorisé), vérifier les résultats et raisonner sur le temps de vie des données.
Ce code ne sera ni compilé ni inséré dans le TP.
71. Coccinelle et transformations sémantiques
Coccinelle utilise un langage de « semantic patches » (SmPL) pour repérer des formes de code C et parfois aider à les transformer. Installation :
Code: Select all
sudo apt install coccinelle
spatch --version
Ne lancez pas une transformation automatique sur un arbre kernel entier sans versionner les fichiers et lire le patch proposé.
72. Smatch et autres outils
Smatch analyse des chemins de code C, notamment des valeurs et contrôles. Sa disponibilité dépend des paquets. Nous l'approfondirons avec de vrais drivers possédant plusieurs chemins d'erreur.
73. Cas pratique : que faut-il analyser ?
Quand une initialisation acquiert A puis B puis échoue sur C, le retour d'erreur doit libérer B puis A si ces ressources ont été obtenues.
Cherchez les retours ignorés, les validations manquantes et les accès concurrents.
74. Journaux, assertions et preuve
Un message « succès » prouve seulement qu'un chemin a été atteint. Il ne démontre ni l'absence de course critique ni la sécurité mémoire. Combinez analyse, logs, tests et relecture.
CHAPITRE 10 - Débogage et traçage
75. Afficher des messages depuis le noyau
pr_info, pr_warn et pr_err émettent des messages noyau selon un niveau de gravité. Ils ne sont pas des alias de printf et leurs messages peuvent être filtrés ou retardés.
76. Lire un problème plutôt que redémarrer au hasard
En cas d'échec d'insmod, notez d'abord le message d'erreur, puis :
Code: Select all
sudo dmesg | tail -n 60
sudo journalctl -k -b --no-pager | tail -n 60
« Unknown symbol » suggère un problème de dépendance ou de résolution de symboles. « Required key not available » renvoie souvent à une politique de signature.
Le diagnostic part des observations réelles, pas d'une solution copiée au hasard.
77. Oops et panic
Un oops peut fournir une pile d'appels et une adresse de faute. Un panic marque un arrêt grave.
La corruption initiale peut précéder le symptôme : il faut lire le contexte, les logs et disposer de symboles adaptés.
78. ftrace : traçage intégré au noyau
ftrace exploite les mécanismes de traçage du kernel. L'interface tracefs est généralement accessible sous /sys/kernel/tracing si le noyau prend en charge la fonctionnalité. Vérifiez :
Code: Select all
ls /sys/kernel/tracing
mount | grep tracefs
Code: Select all
sudo mount -t tracefs tracefs /sys/kernel/tracing
N'exécutez pas de recettes qui activent « tous les événements » : elles peuvent générer beaucoup de données. Pour commencer, inspectez simplement :
Code: Select all
sudo cat /sys/kernel/tracing/available_tracers
sudo head -n 20 /sys/kernel/tracing/available_events
Lire et écrire ces entrées peut interagir directement avec le comportement du noyau.
79. LTTng : une autre approche du tracing
LTTng fournit une infrastructure de capture et d'analyse d'événements. Il est utile pour des traces longues ou transversales, mais reste facultatif pour notre premier module.
80. Mesurer avant d'optimiser
Avant d'optimiser, mesurez : fréquence, durée, charge et contexte. perf ou ftrace peuvent aider. Une instrumentation très bavarde peut modifier les performances observées.
81. eBPF : observer sans écrire nécessairement un .ko
eBPF permet, sous conditions de sécurité et de support du kernel, de charger de petits programmes vérifiés et attachés à divers points d'observation ou d'action.
C'est une famille de technologies distincte de nos modules C ordinaires. Elle sert notamment à la trace, à l'observabilité et à certains traitements réseau.
Elle ne remplace pas tous les drivers matériels. Comprendre le noyau et ses événements rendra l'apprentissage d'eBPF beaucoup plus facile.
82. Espace d'adressage virtuel
Le noyau et les processus travaillent avec des adresses virtuelles dont les correspondances physiques sont gérées via la MMU et les tables de pages.
Dans un programme utilisateur, /proc/<PID>/maps donne une représentation des régions de mémoire du processus.
Il ne s'agit ni d'un plan complet du noyau ni d'une liste de pages physiques.
Code: Select all
cat /proc/self/maps | head -n 10
Les régions vues varient d'une exécution à l'autre, notamment à cause de l'ASLR.
N'essayez pas de déduire une adresse physique d'une adresse virtuelle en inversant simplement les octets d'un pointeur : l'endianness et la traduction d'adresses sont deux sujets différents.
83. Projets et outils de vérification
Les projets de vérification de drivers, ainsi que KUnit, KASAN ou lockdep selon le noyau et sa configuration, aident à détecter différents défauts.
Vérifiez la maintenance et la compatibilité des outils historiques avant usage.
84. Déboguer avec Hyper-V ou QEMU ?
Hyper-V suffit pour les crashes et les logs de nos modules. QEMU peut faciliter des séances de débogage avec GDB en configuration adaptée. Le choix dépend du test, pas d'un classement général.
85. Une méthode de diagnostic reproductible
Pour reproduire une erreur : notez la release, collectez les logs, isolez le cas minimal, modifiez une chose à la fois et conservez une version fonctionnelle. C'est le début d'un diagnostic rigoureux.
CHAPITRE 11 - Compilation croisée et systèmes embarqués
86. Définition
La compilation croisée produit du code pour une cible différente du système qui compile, par exemple ARM64 depuis x86-64. Il faut une toolchain et des sources/configurations adaptées.
87. Hôte, cible et environnement
Le système de build est la machine qui exécute le compilateur. La cible est le système sur lequel le code devra fonctionner.
Notre TP se fait « nativement » dans une VM x86-64 dont le noyau cible est celui de la même VM. Il n'y a donc aucun besoin immédiat de toolchain croisée.
88. ARM, ARM64 et x86-64
x86-64 et ARM64 ont des registres, conventions et règles matérielles différents. Pour les futurs drivers matériels, surveillez les accès MMIO, le DMA et l'ordonnancement mémoire.
89. QEMU et Raspberry Pi
QEMU permet d'émuler certaines machines. Un Raspberry Pi fournit du matériel réel, utile pour GPIO, I2C ou SPI. Nous gardons ces environnements pour une partie ultérieure.
90. Exemple non nécessaire pour ce TP
Une toolchain ARM64 utilise par exemple aarch64-linux-gnu-gcc. Kbuild peut recevoir ARCH=arm64 et CROSS_COMPILE=aarch64-linux-gnu- ; ne les utilisez pas pour le module x86-64 de notre VM.
91. Quand changer d'environnement ?
Gardez Hyper-V pour les modules logiciels et les IOCTL ; envisagez QEMU pour une émulation ou un débogage ciblés, et une carte réelle lorsque le driver dépend du matériel.
CHAPITRE 12 - TP : premier module noyau en C
92. Objectif du premier module Linux
Nous allons créer mon_module.c, un Makefile Kbuild et compiler mon_module.ko.
Ce module logiciel ne crée aucun /dev, ne lit aucun fichier et ne touche à aucun périphérique.
À l'insertion, il écrit un message dans les logs ; au retrait, il écrit un second message.
C'est l'équivalent pédagogique le plus simple d'un cycle « initialisation / nettoyage », sans reproduire les objets WDM.
Avant le TP : créez un point de contrôle Hyper-V et vérifiez le noyau actif.
Code: Select all
uname -r
uname -m
ls -ld /lib/modules/$(uname -r)/build
93. Créer un répertoire de travail
Code: Select all
mkdir -p ~/kernel-lab/hello_module
cd ~/kernel-lab/hello_module
pwd
« ~ » est le dossier personnel de l'utilisateur courant ; les fichiers produits seront donc éditables sans sudo.
94. Code source de mon_module.c
Créez le fichier « mon_module.c » dans ce répertoire et copiez exactement ce code :
Code: Select all
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/init.h>
#include <linux/printk.h>
static int __init mon_module_init(void)
{
pr_info("Module chargé : bonjour depuis le noyau !\n");
return 0;
}
static void __exit mon_module_exit(void)
{
pr_info("Module déchargé : au revoir !\n");
}
module_init(mon_module_init);
module_exit(mon_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Cours Linux Kernel");
MODULE_DESCRIPTION("Module logiciel de demonstration");
95. Explication ligne par ligne
- #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt : définit un préfixe de log dérivé du nom du module, utile pour rechercher ses messages. Cette macro doit être définie avant l'inclusion qui fournit pr_info.
- #include <linux/module.h> : macros et déclarations relatives aux modules et métadonnées.
- #include <linux/init.h> : annotations d'initialisation et de sortie.
- #include <linux/printk.h> : interfaces de messages noyau, dont pr_info.
- static : limite la visibilité du symbole de fonction à ce fichier source ; il ne s'agit pas d'une exportation vers les autres modules.
- int : le callback d'initialisation retourne un entier. 0 indique le succès ; une valeur négative correspondant à une erreur signifie l'échec de l'initialisation.
- __init : annotation utilisée pour les fonctions d'initialisation ; selon le contexte de build, le code d'initialisation peut être placé dans une section particulière et sa mémoire libérée après démarrage réussi.
- pr_info : produit un message de niveau information dans les logs noyau. Le « \n » termine la ligne.
- __exit : annotation utilisée pour le chemin de sortie, notamment lors d'un retrait de module chargeable.
- module_init / module_exit : enregistrent les fonctions qui servent de points d'entrée pour ce module. Ce ne sont pas des appels qui exécutent aussitôt les fonctions pendant la compilation.
- MODULE_LICENSE("GPL") : déclare une licence correspondant au module ; ne confondez pas cette chaîne avec un contrôle juridique automatique du droit d'auteur. Ne déclarez pas faussement la licence d'un code repris d'ailleurs.
- MODULE_AUTHOR, MODULE_DESCRIPTION : métadonnées consultables avec modinfo.
96. Pourquoi ne pas utiliser DriverEntry ?
Sous Windows WDM, DriverEntry reçoit notamment un PDRIVER_OBJECT et un chemin de registre. Ici mon_module_init(void) ne reçoit aucun objet équivalent.
Le chargeur de modules Linux et les infrastructures utilisées par chaque sous-système ont leurs propres mécanismes.
Il n'existe pas de « DRIVER_OBJECT Linux universel » à initialiser comme sous WDM.
97. Écrire le Makefile du module
Créez un fichier nommé exactement « Makefile » (M majuscule) à côté de mon_module.c :
Code: Select all
obj-m += mon_module.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Ici obj-m est pris en compte par Kbuild au moment de la construction du module ; les cibles all/clean constituent l'interface pratique appelée depuis le terminal.
La substitution de commande $(shell uname -r) est celle de make, non du shell.
98. Compiler, sans sudo
Code: Select all
cd ~/kernel-lab/hello_module
make
Une compilation réussie se termine avec le fichier mon_module.ko. Le nombre exact de lignes varie selon la version et les options de Kbuild.
Code: Select all
ls -lh mon_module.ko
file mon_module.ko
modinfo ./mon_module.ko
99. Charger le module
Ouvrez deux terminaux. Dans le premier :
Code: Select all
sudo dmesg -w
Code: Select all
sudo insmod ./mon_module.ko
Le format précis du préfixe, de l'horodatage et de la priorité peut changer. « insmod » ne renvoie pas nécessairement un texte en cas de succès.
Une commande silencieuse ne dispense pas de vérifier lsmod et les logs.
100. Vérifier la présence dans le noyau
Code: Select all
lsmod | grep '^mon_module'
modinfo ./mon_module.ko
Un fichier peut exister sans être chargé. À l'inverse, un module chargé peut ne plus correspondre au fichier local si vous l'avez recompilé après insertion.
101. Retirer le module
Code: Select all
sudo rmmod mon_module
sudo dmesg | tail -n 20
Code: Select all
lsmod | grep '^mon_module' || echo "Module absent"
Ce résultat signifie seulement que le nom recherché n'apparaît pas dans cette sortie, ce qui est conforme à notre exercice.
102. Nettoyer le répertoire
Code: Select all
make clean
ls -la
Gardez le répertoire pour les exercices suivants ; il représentera votre première base de code Linux Kernel.
103. Dépannage des erreurs classiques
Erreur « No such file or directory » pour .../build : headers absents ou différents du noyau démarré.
Revérifiez uname -r et les dépôts ; ne créez pas un lien symbolique arbitraire vers des headers incompatibles.
Erreur « missing separator » dans Makefile : une ligne de recette contient probablement des espaces là où make attend une tabulation. Activez l'affichage des caractères invisibles dans l'éditeur.
Erreur « Invalid module format » : vérifiez les logs dmesg, l'architecture et le champ vermagic ; recompilez contre le bon noyau.
Erreur « Required key not available » : examinez la politique de signature du noyau et l'état Secure Boot.
Sur Ubuntu, vous pouvez consulter « mokutil --sb-state » après installation de mokutil. Des modules non signés ne sont pas forcément acceptés, même s'ils compilent.
Préférez une signature correcte ; à défaut, dans une VM de test isolée uniquement, vous pouvez modifier sa configuration Secure Boot en comprenant le compromis de sécurité.
Erreur « Operation not permitted » : privilèges insuffisants ou restrictions de sécurité. Vérifiez le contexte avant de modifier la configuration du noyau.
Erreur « Module in use » : le noyau refuse le retrait pendant qu'il reste des utilisateurs ou références. Ne forcez pas arbitrairement le retrait d'un driver réel.
104. Exercices progressifs
Exercice A : changez uniquement le message de chargement, recompilez, réinsérez puis observez la différence. Pensez à retirer le module avant de le recharger.
Exercice B : faites retourner -EINVAL depuis mon_module_init après avoir ajouté « #include <linux/errno.h> ».
Préparez un point de contrôle, recompilez puis testez insmod : il doit échouer avec une erreur correspondant à l'argument invalide, et le module ne doit pas rester chargé.
Rétablissez ensuite le retour 0. Ne faites ce test qu'avec ce module volontairement minimal, sans ressources acquises.
Exercice C : notez les différences entre le fichier local .ko, les métadonnées modinfo et la liste lsmod.
Expliquez pourquoi « compiler », « charger » et « exécuter le code d'initialisation » désignent trois événements distincts.
105. Le prochain driver : /dev/mon_driver
Dans la Partie 2, le module deviendra un character device driver.
Il devra enregistrer un périphérique, exposer des opérations via struct file_operations et interagir avec un programme utilisateur.
Le projet final reprendra deux commandes : incrémenter un compteur et lire sa valeur, comme dans l'ancien exercice WDM avec IOCTL_INCREMENT et IOCTL_GET_COUNT.
Nous expliquerons alors précisément la construction des numéros de commande (_IO, _IOR, _IOW, _IOWR), l'usage de copy_to_user/copy_from_user, les validations, les références et la synchronisation.
Rien de cela n'est nécessaire pour le simple « hello module ».
CHAPITRE 13 - Exercices et questions pièges
106. Les dix connaissances à conserver
- 1) Ubuntu est une distribution ; Linux est le noyau.
- 2) Un driver peut être compilé dans le noyau ou en module.
- 3) Un module peut planter la VM : point de contrôle avant insertion.
- 4) uname -r décrit le noyau en cours d'exécution.
- 5) Les headers doivent correspondre au noyau cible.
- 6) Kbuild est l'outil adapté aux modules externes.
- 7) .o et .ko ne sont pas synonymes d'exécutable utilisateur.
- 8) lsmod et modinfo répondent à des questions différentes.
- 9) Un code qui compile peut contenir des défauts de sécurité graves.
- 10) Les API internes du noyau ne possèdent pas d'ABI stable pour les modules externes.
107. Questions de compréhension
- Q1. Pourquoi faut-il redémarrer après une mise à jour du noyau avant de construire un module contre la nouvelle release ?
- Q2. Pourquoi l'IDE KDevelop ne suffit-il pas à déterminer les bonnes options du compilateur pour un module ?
- Q3. Quelle est la différence entre insmod et modprobe ?
- Q4. Quelles commandes montrent la release active et les métadonnées d'un module ?
- Q5. Pourquoi la lecture de « man 2 ioctl » ne suffit-elle pas à écrire la fonction ioctl d'un driver ?
108. Questions pièges
- PIÈGE 1 : « mon_module.ko existe donc il fonctionne. » Faux : la construction peut réussir, mais le chargement ou l'exécution échouer.
- PIÈGE 2 : « Linux 6.x garantit la compatibilité de mes modules externes. » Faux : l'ABI interne n'est pas stable.
- PIÈGE 3 : « Tout driver Linux reçoit des IRP. » Faux : les IRP appartiennent au modèle Windows WDM.
- PIÈGE 4 : « Le code noyau peut utiliser printf sans problème. » Faux : printf appartient à la libc utilisateur.
- PIÈGE 5 : « Un module GPL ne peut jamais marquer le noyau comme tainted. » Faux : d'autres causes de taint existent, dont certains modules externes.
- PIÈGE 6 : « Un checkpoint remplace mes sauvegardes. » Faux : il protège un état de VM, pas toutes les données externes.
109. Réponses commentées aux questions
- R1. uname -r ne change qu'après démarrage d'un nouveau noyau ; les headers doivent suivre le noyau ciblé.
- R2. Les fichiers générés, macros et options de compilation dépendent de la configuration du kernel et de Kbuild ; l'indexation de l'IDE est auxiliaire.
- R3. insmod charge le fichier fourni ; modprobe s'appuie sur les bases et dépendances de modules installés.
- R4. uname -r et modinfo ; lsmod vérifie séparément l'état chargé.
- R5. La documentation du syscall utilisateur ne décrit pas l'ensemble de la structure ni des contraintes de callback d'un driver.
110. Avant de passer à la Partie 2
Avant la Partie 2, vérifiez que vous savez créer et restaurer une VM, identifier la release, installer les headers, comprendre le Makefile, compiler et examiner un .ko, le charger, lire ses messages et le retirer.
Comprendre un échec est plus important que mémoriser une commande.
Résumé
Pour développer un module Linux, il faut disposer d'une VM Ubuntu, du compilateur GCC, de Make, et des en-têtes correspondant au noyau exécuté.
KDevelop sert à écrire le code, mais la compilation d'un module doit être effectuée avec Kbuild.
Un module doit être compilé, inspecté, chargé et retiré en vérifiant les messages du noyau.
Code: Select all
uname -r
make
modinfo ./mon_module.ko
sudo insmod ./mon_module.ko
lsmod | grep mon_module
sudo dmesg | tail -n 20
sudo rmmod mon_module
Documentation à consulter
- Documentation du noyau : https://docs.kernel.org/
- Compilation de modules externes : https://docs.kernel.org/kbuild/modules.html
- Documentation Ubuntu : https://documentation.ubuntu.com/
- Hyper-V : https://learn.microsoft.com/windows-ser ... n/hyper-v/
