\thispagestyle{empty} Une *segfault* -- *segmentation fault*, ou *erreur de segmentation* -- apparaît quand un programme essaye d'accéder à une zone mémoire qui ne lui est pas allouée\footnote{À noter que si une segfault est toujours due à un accès à de la mémoire non allouée, l'inverse n'est pas vrai : parfois, un tel accès ne résulte pas en une segfault, et peut par exemple corrompre une autre variable du programme.}. C'est une erreur fréquente dès lors qu'on manipule des pointeurs, et elle est plus difficile à corriger qu'un bug classique -- en tout cas quand on n'a pas les bons réflexes ! Elle arrive par exemple dans les cas suivants : \Begin{minipage}{0.3\textwidth} ```c int *test = NULL; *test = 42; ``` \End{minipage} \hfill \Begin{minipage}{0.3\textwidth} ```c int *test = NULL; printf("%d\n", *test); ``` \End{minipage} \hfill \Begin{minipage}{0.3\textwidth} ```c int *test = 215342565; *test = 42; ``` \End{minipage} Dans le premier cas, on essaye d'assigner une valeur à l'adresse mémoire `NULL` -- c'est-à-dire, l'adresse 0, ce qui est une erreur. De même, dans le second cas, on essaye de lire cette valeur -- c'est tout autant interdit. Dans le troisième cas, on fait de même, mais à une adresse au hasard -- les chances que cette adresse corresponde à de la mémoire qui nous est réservée sont infimes ! Prenons l'exemple suivant : \Begin{minipage}{0.39\textwidth} ```c struct liste { struct liste* suivant; int valeur; }; typedef struct liste liste_t; // Alloue une nouvelle cellule de // valeur `val` liste_t* alloc_cell(int val) { liste_t* cell = malloc(sizeof(liste_t)); cell->valeur = val; cell->suivant = NULL; return cell; } ``` \End{minipage}\hfill\Begin{minipage}{0.58\textwidth} ```c // Affiche les `taille` 1ères valeurs de `liste` void afficher_liste(liste_t* liste, int taille) { liste_t* cur = liste; for(int pos=0; pos < taille; ++pos) { printf("%d\n", cur->valeur); cur = cur->suivant; } } int main(int argc, char** argv) { int n = atoi(argv[1]); // premier argument liste_t* tete = alloc_cell(1); tete->suivant = alloc_cell(2); afficher_liste(tete, n); return 0; } ``` \End{minipage} Ce programme n'est pas correct si on lui passe `n > 2` : on tente d'afficher plus de valeurs de la liste qu'il n'y en a. Et en effet : ```GDB $ ./test 3 1 2 Segmentation fault (core dumped) ``` # Premier réflexe : `valgrind` [Valgrind](https://fr.wikipedia.org/wiki/Valgrind) est un logiciel analysant les accès mémoire d'un programme. Il peut servir de débuggeur rudimentaire, mais analyse également les *fuites mémoire*. Il s'utilise très facilement : il suffit de rajouter `valgrind` en tête de sa ligne de commande. ```GDB $ valgrind ./test 3 [...] ==862386== Process terminating with default action of signal 11 (SIGSEGV): dumping core ==862386== Access not within mapped region at address 0x8 ==862386== at 0x1091CE: afficher_liste (exemple.c:20) ==862386== by 0x109253: main (exemple.c:29) [...] ``` Valgrind commence par nous rappeler qu'il s'agit d'une *segfault* : `SIGSEGV` est encore un de ses autres noms\footnote{C'est en réalité plutôt le nom de la réaction de Linux face à une segfault -- \textit{cf.} les \textit{signaux UNIX} si vous êtes curieux·se.}. Dans les lignes *avant* ce message (éludées ici), valgrind liste les erreurs non-critiques rencontrées. Bien souvent, ces erreurs correspondent se traduisent par des problèmes incompréhensibles plus tard, et mieux vaut les inspecter de plus près ! Pour une *segfault*, la partie la plus importante est celle recopiée ici. Elle nous indique, de bas en haut, que : * dans la fonction `main`, fichier `exemple.c`, ligne 29, … * on appelle la fonction `afficher_liste`, fichier `exemple.c`, et ligne 20… * on crash (segfault) La première ligne nous indique la position du bug, les lignes suivantes nous indiquent d'où on vient dans le programme. C'est certes peu d'informations (la ligne de l'erreur seulement -- pas la variable concernée, etc), mais c'est déjà mieux que juste `Segmentation fault (core dumped)`, et souvent suffisant. # Si c'est plus compliqué : gdb Le debugger `gdb` est plus compliqué à utiliser, mais plus puissant. Lorsqu'on l'appelle ```GDB $ gdb ./test [...] (gdb) ``` on entre en *mode interactif* : gdb attend des commandes. Notez qu'on ne **passe pas nos arguments à gdb** : `gdb ./test` et non `gdb ./test 3`. Il faut tout d'abord dire à gdb de lancer le programme (c'est ici qu'on passe le 3 !) ```GDB (gdb) run 3 [...] Program received signal SIGSEGV, Segmentation fault. 0x00005555555551ce in afficher_liste (tete=0x5555555592a0, taille=3) at exemple.c:20 20 printf("%d\n", cur->valeur); (gdb) ``` `gdb` nous dit qu'on a une *segfault* dans `afficher_liste`, ligne 20 de `exemple.c` -- rien de nouveau. Il nous donne les valeurs des arguments de cette fonction (`taille=3`, et une adresse pour `tete`). Mais surtout, *on reste en mode interactif* ! On peut lui demander ce que `valgrind` affichait spontanément, la *backtrace* (savoir d'où on vient) : ```GDB (gdb) backtrace #0 0x00005555555551ce in afficher_liste (tete=0x5555555592a0, taille=3) at exemple.c:20 #1 0x0000555555555254 in main (argc=2, argv=0x7fffffffe4b8) at exemple.c:29 ``` On peut lui demander d'afficher des valeurs *au moment du crash* : ```GDB (gdb) print pos 2 (gdb) print cur (liste_t *) 0x0 (gdb) print &(liste->suivant) # On peut écrire des expressions C compliquées (struct liste **) 0x5555555592a0 (gdb) print liste->suivant->valeur + 42 - taille # aussi compliquées qu'on veut 41 ``` ...ou la même chose, mais dans `main`, au moment de l'appel de fonction : ```GDB (gdb) frame 1 # le même 1 qu'en début de ligne de `backtrace` (gdb) print n 3 (gdb) frame 0 # et de retour dans `afficher_liste` ``` Le debugger `gdb` est *beaucoup* plus puissant que ça, avec des dizaines de fonctionnalités : renseignez-vous sur les *breakpoints* par exemple. Il est également très pratique pour débugger des erreurs classiques, et pas seulement des *segfaults*.