Diagnostic et analyse des plantages (Oops) du noyau Linux

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 00000000 indiquent la nature du problème (par exemple, un déréférencement de pointeur nul) et, si le code utilise BUG() ou BUG_ON(), le fichier source et le numéro de ligne.
  • Les valeurs des registres PC et LR : PC is at __bug+0x20/0x28 et LR 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, fp et 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/0x400 signifie que l'exécution était à 0x3f8 octets de l'entrée de la fonction skb_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 inline ou 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.

Étiquettes: Linux noyau débogage Oops Crash

Publié le 24 octobre à 02h19