183 lines
5.6 KiB
Markdown
183 lines
5.6 KiB
Markdown
|
\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. 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 la même chose qu'à `valgrind`,
|
||
|
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
|
||
|
```
|
||
|
|
||
|
...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*.
|