1. Introduction

Tout développeur C ou C++ a souvent déjà rencontré le message « Segmentation fault (core dumped) ». Il peut se produire suite à un accès mémoire incorrect, une erreur de calcul en virgule flottante, etc.

Pour identifier l'origine du problème, il faut lancer le programme sous débogueur ou s'appuyer sur le fichier core dump.

2. Core dump post mortem

Lors du chargement du programme, le système organise la mémoire du processus, place le code dans la section .text, les données initialisées dans la section.data, et les données non initialisées dans la section .bss. Il agence aussi des pages mémoire pour la pile et le tas, et ceci pour chaque objet partagé. On peut configurer le système pour qu'il effectue un vidage mémoire dans un fichier en cas de crash : le core dump.

Ces informations contiennent :

  • la section .data au moment du crash ;
  • la section .bss ;
  • le tas ;
  • la pile ;
  • le contenu des registres ;
  • le contenu de la pile d'appels (back-trace, permettant de lister les différentes adresses empilées, et permettant donc de remonter dans les différents appels de fonction) ;
  • et plus encore…

Pour que ce fichier puisse être créé, exécutez :

 
Sélectionnez
# ulimit -c unlimited

Après un crash, vous trouverez dans le répertoire courant de l'application un fichier core que vous pourrez charger avec votre débogueur pour analyser le problème :

 
Sélectionnez
$ gdb ./app ./core
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./app...done.
[New LWP 16425]
Core was generated by `./app`.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400837 in main () at ./myapp.c:67
67        *p=100;
(gdb) l
64        
65        p=0x90;
66        
67        *p=100;
68        
69        printf("%d\n",z);
70        return 0;
71    }
(gdb)

Le débogueur indique le problème (SIGSEV), l'adresse, le fichier source et le numéro de ligne de celui-ci (myapp.c:67).

Vous pouvez examiner les registres (info registers) ou la mémoire (x/20w $rsp), consulter la pile d'appels (backtrace - bt) et plus.

2-1. Problèmes avec les fichiers core dump

On rencontre deux problèmes principaux avec les fichiers core dump :

  1. Ils peuvent être très gros si le programme a consommé beaucoup de mémoire avant de se crasher. C'est un problème quand le core dump a été généré sur la machine d'un utilisateur et que nous devons le transférer vers celle du développeur. Ça peut aussi être un problème majeur si l'espace de stockage est limité comme, par exemple, dans des systèmes embarqués disposant d'un Go de RAM, mais de seulement 256 Mo de mémoire flash. Parfois, le système de fichiers est en lecture seule (système de fichiers racine sur les mobiles sous Android) ;

  2. Lors de l'examen du core dump, nous ne pouvons voir que les données relatives au CPU. Si vous avez par exemple du matériel mappé sur une plage d'adresses virtuelles, vous ne pouvez pas accéder à celle-ci en utilisant le core dump. En d'autres termes, le core dump n'enregistre que les informations du CPU et pas celles des autres registres matériels (registres FPGA, etc.).

3. Écriture d'un gestionnaire d'erreurs (Fault handler)

Pour résoudre le problème exposé auparavant, vous pouvez écrire un gestionnaire de signal traitant certains des signaux causés par des erreurs :

 
Sélectionnez
void setHandler(void (*handler)(int,siginfo_t *,void *))
{
    struct sigaction action;
    action.sa_flags = SA_SIGINFO;
    action.sa_sigaction = handler;
 
    if (sigaction(SIGFPE, &action, NULL) == -1) {
        perror("sigfpe: sigaction");
        _exit(1);
    }
    if (sigaction(SIGSEGV, &action, NULL) == -1) {
        perror("sigsegv: sigaction");
        _exit(1);
    }
    if (sigaction(SIGILL, &action, NULL) == -1) {
        perror("sigill: sigaction");
        _exit(1);
    }
    if (sigaction(SIGBUS, &action, NULL) == -1) {
        perror("sigbus: sigaction");
        _exit(1);
    }
 
}

Cette fonction utilitaire reçoit un pointeur de fonction et le définit pour être le gestionnaire de signal pour SIGSEGV (accès mémoire invalide), SIGFPE (erreur de calcul en virgule flottante, division par zéro), SIGILL (instruction illégale ou privilégiée) et SIGBUS (erreur de bus, en général une tentative d'accès à une adresse inaccessible).

Écrivons maintenant un gestionnaire simple et utilisons-le :

 
Sélectionnez
void fault_handler(int signo, siginfo_t *info, void *extra) 
{
    printf("Signal %d reçu\n", signo);
    abort();
}
 
 
int main()
{
    int *p=NULL;
 
    setHandler(fault_handler);    
    
    *p=100;
    return 0;
}

Dans cet exemple, nous affichons simplement un message et abandonnons. Nous pouvons afficher ce que nous voulons dans le gestionnaire, par exemple le contenu d'une région mémoire mappée sur du matériel.

 
Sélectionnez
void fault_handler(int signo, siginfo_t *info, void *extra) 
{
    int i;
    printf("Signal %d reçu\n", signo);
    for(i=0; i < hw_size ; i++)
        printf("HW regs[%d] = %x\n", i, hwmap[i]);
    abort();
}

Vous pouvez aussi utiliser les données fournies par le paramètre siginfo_t, par exemple sur SIGFPE pour indiquer l'adresse de l'instruction fautive :

 
Sélectionnez
printf("adresse siginfo=%x\n",info->si_addr);

Notez qu'avec un SIGSEGV, si_addr sera l'adresse fautive (l'adresse vers laquelle nous avons tenté un accès, NULL dans l'exemple ci-dessus).

4. Le contexte d'erreur

Le plus important avec le gestionnaire d'erreurs est son troisième paramètre. Nous le déclarons comme un void* dans le prototype, mais son véritable type ucontext_t. La raison de cette déclaration en void* est la dépendance de ucontext_t à l'architecture cible. Vous pouvez trouver sa structure dans son fichier d'en-tête et l'utiliser.

Si nous voulons par exemple afficher le pointeur d'instruction (Instruction Pointer) de l'instruction fautive sur x86 64 bits, nous écrirons :

 
Sélectionnez
void fault_handler(int signo, siginfo_t *info, void *extra) 
{
    ucontext_t *p=(ucontext_t *)extra;
    int val;
    printf("Signal %d reçu\n", signo);
    printf("siginfo adresse=%x\n",info->si_addr);
 
    val= p->uc_mcontext.gregs[REG_RIP];
 
    printf("adresse = %x\n",val);
 
    abort();
 
}

Le tableau gregs contient la valeur de tous les registres à usage général.

Nous pouvons utiliser cette méthode pour accéder à tous les registres (RSP , RBP, RAX, etc.)

Si nous écrivons ce gestionnaire pour l'architecture ARM, le code sera différent :

 
Sélectionnez
void fault_handler(int signo, siginfo_t *info, void *extra) 
{
    ucontext_t *p=(ucontext_t *)extra;
    int val;
    printf("Signal %d reçu du père\n", signo);
    printf("siginfo adresse=%x\n",info->si_addr);
 
    val= p->uc_mcontext.arm_pc;
 
    printf("adresse = %x\n",val);
 
    abort();
 
}

De la même façon, nous pouvons accéder aux autres registres : arm_sp , arm_lr, arm_r0, etc.

Consultez le fichier d'en-tête de l'architecture cible pour plus de détails.

Si nous exécutons le code ci-dessus, nous obtiendrons :

 
Sélectionnez
Developpeur@:~exemples/exemples_debogage/faute_ex$ ./app
Signal 11 reçu du père
siginfo adresse=0
adresse=400867
Abort(core dumped)

Nous pouvons voir que siginfo retourne l'adresse fautive (NULL), mais que la valeur du registre RIP est 0x400867.

Nous pouvons maintenant charger le programme dans le débogueur pour voir vers quoi cette adresse pointe :

on utilise la commande list en lui passant l'adresse de l'erreur : (gdb) list  *0x400867.

Image non disponible

Comme vous pouvez le voir, elle affiche le nom du fichier en cause et le numéro de ligne de l'erreur (fa.c:67).

5. Intégration au core dump

Si le gestionnaire d'erreurs retourne à l'appelant, on entrera dans une boucle sans fin. C'est pour cela que nous exécutons abort ou exit à la fin. L'appel système abort génère un core dump, mais dans le contexte du gestionnaire de signal. Une astuce que nous pouvons utiliser est de remettre le gestionnaire de signal par défaut, cela produira un core dump à la position originale de l'erreur :

 
Sélectionnez
void fault_handler(int signo, siginfo_t *info, void *extra) 
{
    ucontext_t *p=(ucontext_t *)extra;
    int x;
    printf("Signal %d reçu du père\n", signo);
    printf("Adresse siginfo=%x\n",info->si_addr);

    x= p->uc_mcontext.gregs[REG_RIP];

    printf("adresse = %x\n",x);

    setHandler(SIG_DFL);

}

Ce qui donne :

 
Sélectionnez
Developpeur@:~/exemples/exemple_debogage/ex_faute$ ./app
Signal 11 reçu du père
Adresse siginfo=0
adresse = 40082f
Segmentation fault (core dumped)

Le core dump a été généré par la ligne erronée (et pas par abort).

6. Astuce : Utilisation de LD_PRELOAD pour intégrer notre gestionnaire dans n'importe quel programme existant

Vous pouvez intégrer le gestionnaire d'erreurs dans une bibliothèque partagée et utiliser l'attribut constructor pour l'initialiser. Vous pourrez alors précharger la bibliothèque dans tout programme en utilisant LD_PRELOAD.

Voici le code de la bibliothèque partagée :

 
Sélectionnez
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
 
#include <sys/ucontext.h>
#include <ucontext.h>
 
static void __attribute__ ((constructor)) init_lib(void);
 
void setHandler(void (*handler)(int,siginfo_t *,void *))
{
    struct sigaction action;
    action.sa_flags = SA_SIGINFO;
    action.sa_sigaction = handler;
 
    if (sigaction(SIGFPE, &action, NULL) == -1) {
        perror("sigusr: sigaction");
        _exit(1);
    }
    if (sigaction(SIGSEGV, &action, NULL) == -1) {
        perror("sigusr: sigaction");
        _exit(1);
    }
    if (sigaction(SIGILL, &action, NULL) == -1) {
        perror("sigusr: sigaction");
        _exit(1);
    }
    if (sigaction(SIGBUS, &action, NULL) == -1) {
        perror("sigusr: sigaction");
        _exit(1);
    }
 
}
 
void catchit(int signo, siginfo_t *info, void *extra) 
{
    ucontext_t *p=(ucontext_t *)extra;
    int x;
    printf("Signal %d reçu\n", signo);
    printf("Adresse siginfo=%x\n",info->si_addr);
 
    x= p->uc_mcontext.gregs[REG_RIP];
 
    printf("adresse = %x\n",x);
 
    setHandler(SIG_DFL);
 
}
 
 
void init_lib()
{
    setHandler(catchit);
}

Nous le compilons pour obtenir une bibliothèque :

 
Sélectionnez
# gcc -shared -o libFault.so ./fa_lib.c -fPIC

Le code de l'application (il n'y a plus de gestionnaire intégré) :

 
Sélectionnez
#include <stdio.h>
int main()
{
    int x=9,y=0,z;
    int *p=NULL;
    
    *p=100;
    z=x/y;
    printf("%d\n",z);
    return 0;
}

Compilons-le simplement comme ceci :

 
Sélectionnez
# gcc -o appnosig ./example.c

Exécutons-le tel quel :

 
Sélectionnez
# ./appnosig 
Segmentation fault (core dumped)

Injectons la bibliothèque partagée :

 
Sélectionnez
# LD_PRELOAD=./libFault.so ./appnosig 
Signal 11 reçu
Adresse siginfo=0
adresse = 400548
Segmentation fault (core dumped)

7. En cas d'erreur, reprise sur l‘instruction suivante

Dans certains cas, nous avons besoin que notre programme continue à fonctionner même s'il y a eu un problème. Nous pouvons gérer cela de plusieurs façons :

  1. Continuer ailleurs et ne jamais revenir du gestionnaire d'erreurs ;
  2. Utiliser l'appel système execve(2) ou un de ces wrappers pour appeler une autre fonction principale ;
  3. Changer le pointeur d'instruction du contexte pour que le retour s'effectue vers un autre endroit du code :
 
Sélectionnez
void continue_after_crash(void)
{
    printf("fonctionnement en mode normal maintenant\n");
    exit(0);
}
 
void catchit(int signo, siginfo_t *info, void *extra) 
{
    ucontext_t *p=(ucontext_t *)extra;
    int x;
    printf("Signal %d reçu depuis le père\n", signo);
    printf("Adresse siginfo=%x\n",info->si_addr);
 
    x= p->uc_mcontext.gregs[REG_RIP];
 
    printf("adresse = %x\n",x);
 
    p->uc_mcontext.gregs[REG_RIP] = (unsigned int)continue_after_crash;
 
}
  1. Sortie :

     
    Sélectionnez
    Developpeur@:~/exemples/exemples_debogage/ex_faute$ ./app
    Signal 11 reçu du père
    Adresse siginfo=0
    adresse = 4008cf
    fonctionnement en mode normal maintenant
  2. Ignorer la ligne erronée et sauter à la suivante. C‘est dangereux et très dépendant de la plateforme. Vous devez connaître le nombre d'octets à ignorer pour atteindre la prochaine instruction. Par exemple sur Ubuntu 64 bits :
 
Sélectionnez
void catchit(int signo, siginfo_t *info, void *extra) 
{
    ucontext_t *p=(ucontext_t *)extra;
    int x;
    printf("Signal %d reçu du père\n", signo);
    printf("siginfo address=%x\n",info->si_addr);
 
    x= p->uc_mcontext.gregs[REG_RIP];
//    x= p->uc_mcontext.arm_pc;
 
    printf("adresse = %x\n",x);
 
    p->uc_mcontext.gregs[REG_RIP] += 6;
    //setHandler(SIG_DFL);
    //p->uc_mcontext.gregs[REG_RIP] = (unsigned int)continue_after_crash;
    //abort();
 
}
 
 
int main()
{
    int x=9,y=0,z=10;
    int *p=NULL;
 
    setHandler(catchit);
    
    
    *p=100; 
    printf("Toujours vivant ??? \n");
    z=x/y;
    printf("%d\n",z);
    return 0;
}
  1. Sortie :
 
Sélectionnez
Developpeur@:~/exemples/exemples_debogage/ex_faute$ ./app
Signal 11 reçu du père
Adresse siginfo = 0
adresse = 4008ce
Toujours vivant ?
Signal 8 reçu du père
Adresse siginfo = 4008f2
adresse = 4008f2
10

Comme vous pouvez le voir, le programme a sauté par-dessus l'affectation du pointeur à NULL puis la division par zéro. Ce n'est pas tellement utile, mais c'est possible.

Vous pouvez voir le code complet de l'exemple ici.

8. Notes de la rédaction

La rédaction remercie Liran B.H pour son autorisation de publication de :

« Linux - Writing Fault handlers ».

La rédaction remercie Chrtophe pour sa traduction et jlliagre pour sa relecture technique, ainsi que ClaudeLELOUP pour sa correction orthographique.