inf301-debug_segfaults/poly/poly.md

188 lines
6 KiB
Markdown
Raw Permalink Normal View History

2021-11-22 15:04:41 +01:00
\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
2021-11-22 15:41:11 +01:00
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 !
2021-11-22 15:04:41 +01:00
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
2021-11-22 15:41:11 +01:00
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.
2021-11-22 15:04:41 +01:00
# 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
2021-11-22 15:41:11 +01:00
reste en mode interactif* ! On peut lui demander ce que `valgrind` affichait spontanément,
la *backtrace* (savoir d'où on vient) :
2021-11-22 15:04:41 +01:00
```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
2021-11-22 15:41:11 +01:00
(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
2021-11-22 15:04:41 +01:00
```
...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*.