Les plantages du noyau Linux, souvent manifestés par des messages Oops ou des Kernel Panic, sont des événements critiques lors du développement ou du déploiement de pilotes ou de modules. Ces défaillances peuvent survenir pour diverses raisons, telles que des déréférencements de pointeurs nuls ou des accès mémoire hors limites. La méthode la plus courante pour identifier la cause et l'emplacement de ces problèmes est l'analyse de la trace de pile (stack trace) générée lors de l'incident.
Lorsqu'une erreur de type Oops se produit, le système affiche généralement un journal détaillé sur la console série ou via la commande dmesg. Voici un exemple typique de journal Oops provenant d'un noyau Linux sur architecture ARM :
<2>[515753.310000] kernel BUG at net/core/skbuff.c:1846!
<1>[515753.310000] Unable to handle kernel NULL pointer dereference at virtual address 00000000
<1>[515753.320000] pgd = c0004000
<1>[515753.320000] [00000000] *pgd=00000000
<0>[515753.330000] Internal error: Oops: 817 [#1] PREEMPT SMP
<0>[515753.330000] last sysfs file: /sys/class/net/eth0.2/speed
<4>[515753.330000] module: http_timeout bf098000 4142
...
<4>[515753.330000] CPU: 0 Tainted: P (2.6.36 #2)
<4>[515753.330000] PC is at __bug+0x20/0x28
<4>[515753.330000] LR is at __bug+0x1c/0x28
<4>[515753.330000] pc : [<c01472d0>] lr : [<c01472cc>] psr: 60000113
<4>[515753.330000] sp : c0593e20 ip : c0593d70 fp : cf1b5ba0
<4>[515753.330000] r10: 00000014 r9 : 4adec78d r8 : 00000006
<4>[515753.330000] r7 : 00000000 r6 : 0000003a r5 : 0000003a r4 : 00000060
<4>[515753.330000] r3 : 00000000 r2 : 00000204 r1 : 00000001 r0 : 0000003c
<4>[515753.330000] Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment kernel
<4>[515753.330000] Control: 10c53c7d Table: 4fb5004a DAC: 00000017
<0>[515753.330000] Process swapper (pid: 0, stack limit = 0xc0592270)
<0>[515753.330000] Stack: (0xc0593e20 to 0xc0594000)
<0>[515753.330000] 3e20: ce2ce900 c0543cf4 00000000 ceb4c400 000010cc c8f9b5d8 00000000 00000000
<0>[515753.330000] 3e40: 00000001 cd469200 c8f9b5d8 00000000 ce2ce8bc 00000006 00000026 00000010
...
<4>[515753.330000] [<c01472d0>] (PC is at __bug+0x20/0x28)
<4>[515753.330000] [<c01472d0>] (__bug+0x20/0x28) from [<c0543cf4>] (skb_checksum+0x3f8/0x400)
<4>[515753.330000] [<c0543cf4>] (skb_checksum+0x3f8/0x400) from [<bf11a8f8>] (et_isr+0x2b4/0x3dc [et])
<4>[515753.330000] [<bf11a8f8>] (et_isr+0x2b4/0x3dc [et]) from [<bf11aa44>] (et_txq_work+0x24/0x54 [et])
<4>[515753.330000] [<bf11aa44>] (et_txq_work+0x24/0x54 [et]) from [<bf11aa88>] (et_tx_tasklet+0x14/0x298 [et])
<4>[515753.330000] [<bf11aa88>] (et_tx_tasklet+0x14/0x298 [et]) from [<c0171510>] (tasklet_action+0x12c/0x174)
<4>[515753.330000] [<c0171510>] (tasklet_action+0x12c/0x174) from [<c05502b4>] (__do_softirq+0xfc/0x1a4)
<4>[515753.330000] [<c05502b4>] (__do_softirq+0xfc/0x1a4) from [<c0171c98>] (irq_exit+0x60/0x64)
<4>[515753.330000] [<c0171c98>] (irq_exit+0x60/0x64) from [<c01431fc>] (do_local_timer+0x60/0x74)
<4>[515753.330000] [<c01431fc>] (do_local_timer+0x60/0x74) from [<c054f900>] (__irq_svc+0x60/0x10c)
<4>[515753.330000] Exception stack(0xc0593f68 to 0xc0593fb0)
Élémants clés à examiner dans un journal Oops
Pour un diagnostic efficace, portez une attention particulière aux informations suivantes :
- Le message d'erreur principal : Des lignes comme
kernel BUG at net/core/skbuff.c:1846! Unable to handle kernel NULL pointer dereference at virtual address 00000000indiquent la nature du problème (par exemple, un déréférencement de pointeur nul) et, si le code utiliseBUG()ouBUG_ON(), le fichier source et le numéro de ligne. - Les valeurs des registres PC et LR :
PC is at __bug+0x20/0x28etLR is at __bug+0x1c/0x28. Le registre PC (Program Counter) pointe vers l'instruction qui a causé l'Oops, tandis que LR (Link Register) contient l'adresse de retour, permetant souvent de remonter à l'appelant. - L'identifiant du CPU et les registres CPU : Les valeurs de
sp,ip,fpet des registres généraux (r0-r10) fournissent un aperçu de l'état du processeur au moment du crash. - Le contexte du processus :
Process swapper (pid: 0, stack limit = 0xc0592270)indique le processus en cours d'exécution lorsque le plantage s'est produit. Cela est crucial si l'erreur est liée à un contexte utilisateur spécifique ou à un processus système. - La trace de pile (Stack Trace) : C'est la partie la plus importante. Elle liste les fonctions appelées, du point du plantage jusqu'à l'origine de la séquence d'appels. Chaque entrée est typiquement au format
<adresse> (nom_fonction+offset/taille_totale [module]). Par exemple,skb_checksum+0x3f8/0x400signifie que l'exécution était à 0x3f8 octets de l'entrée de la fonctionskb_checksum, dont la taille totale est de 0x400 octets. Cet offset est essentiel pour une localisation précise.
Localisation précise avec objdump
Pour déterminer la ligne de code source exacte correspondant à un offset donné dans la trace de pile, l'outil de désassemblage objdump est indispensable. Voici comment l'utiliser :
objdump -D -S nom_du_module.o > trace_desassemblee.txt
L'option -D désassemble toutes les sections, et -S tente d'intercaler le code source C/C++ avec le code assembleur. Pour que l'option -S soit efficace, il est impératif que les fichiers objet aient été compilés avec les symboles de débogage activés (option -g du compilateur GCC).
Activation des symboles de débogage dans le noyau Linux
Pour inclure les informations de débogage lors de la compilation du noyau, modifiez le fichier Makefile à la racine de votre arborescence du noyau. Recherchez la variable KBUILD_CFLAGS et ajoutez l'option -g :
KBUILD_CFLAGS := -g -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
-fno-strict-aliasing -fno-common \
-Werror-implicit-function-declaration \
-Wno-format-security \
-fno-delete-null-pointer-checks -Wno-implicit-function-declaration \
-Wno-unused-but-set-variable \
-Wno-unused-local-typedefs
Recompilez ensuite le module ou le noyau pour que ces modifications prennent effet.
Exemple de désassemblage avec code source
Une fois les symboles de débogage inclus, le fichier de sortie de objdump ressemblera à ceci, intercalant le code source C avec les instructions assembleur correspondantes :
000001f0 <et_isr>:
irqreturn_t et_isr(int irq, void *dev_id)
{
1f0: e92d40f8 push {r3, r4, r5, r6, r7, lr}
1f4: e1a04001 mov r4, r1
struct et_info *et_data = (struct et_info *)dev_id;
struct chops *fonctions_chops;
uint evenements = 0;
fonctions_chops = et_data->etc->chops;
1f8: e5913000 ldr r3, [r1]
if (!et_data->etc->up) {
1fc: e5d32028 ldrb r2, [r3, #40] ; 0x28
200: e3520000 cmp r2, #0
204: 1a000001 bne 218 <et_isr+0x28>
goto fin_isr;
}
// Récupérer les bits de condition d'interruption
evenements = (*fonctions_chops->getintrevents)(et_data->etc->ch, TRUE);
220: e5963028 ldr r3, [r6, #40] ; 0x28
224: e1a00005 mov r0, r5
228: e3a01001 mov r1, #1
22c: e12fff33 blx r3
230: e1a07000 mov r7, r0
// ... plusieurs lignes ...
// Appel à la fonction de calcul de checksum
skb->csum = skb_checksum(skb, thoff, skb->len - thoff, 0);
49c: e5952070 ldr r2, [r5, #112] ; 0x70
4a0: ebfffffe bl 0 <skb_checksum>
4a4: e585007c str r0, [r5, #124] ; 0x7c
// ...
}
En utilisant la trace de pile du journal Oops, par exemple (et_isr+0x2b4/0x3dc [et]), et l'adresse d'entrée de la fonction et_isr (ici 0x1f0), on peut calculer l'adresse exacte de l'instruction fautive : 0x1f0 + 0x2b4 = 0x4a4. En consultant la sortie d'objdump, nous trouvons à l'adresse 0x4a4 l'instruction str r0, [r5, #124], qui suit l'appel à skb_checksum. Cela nous permet d'identifier précisément la ligne de code C concernée : skb->csum = skb_checksum(...).
Considérations importantes
- Fiabilité des offsets : Bien que la fonction appelante dans la trace de pile puisse parfois être imprécise (par exemple, si le registre LR est modifié manuellement), l'offset à l'intérieur de la fonction indiquée est généralement fiable pour localiser l'instruction.
- Fonctions inlines et optimisations : Les fonctions déclarées
inlineou celles fortement optimisées par le compilateur peuvent ne pas apparaître dans la trace de pile, car leur code est directement intégré à l'appelant, éliminant la création d'une nouvelle frame de pile.