Débogage avancé/Travail pratique/Observation à la loupe
But de ce TP
modifierUn des bugs classiques en C et C++ est de lire ou d'écrire en dehors d'une zone préalablement allouée. Suivant les cas les comportements à l'exécution sont un peu différents. Cela va permettre d'illustrer différentes techniques sur des bugs très similaires.
Les trois codes à déboguer
modifierLire un peu avant la zone allouée
modifierCréer un fichier bug_overreadheap_tiny.c contenant le code suivant.
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const uint32_t NB = 100;
void fibon(const uint32_t n, uint32_t p[static n]) {
// C11 array args, p is guaranteed by programmer to be at least n
for (uint32_t i = 0; i < n; i++)
if (i < 1) // BUG: i < 2
p[i] = i;
else
p[i] = p[i - 1] + p[i - 2]; // BUG: read p[-1]
}
int main() {
assert(NB > 2);
uint32_t *p = malloc(sizeof(uint32_t[NB]));
assert(p != NULL);
memset(p, 0, sizeof(uint32_t[NB]));
fibon(NB, p);
free(p);
return EXIT_SUCCESS;
}
Écrire un peu après la zone allouée
modifierCréer un fichier bug_overwriteheap_tiny.c contenant le code suivant.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
const unsigned int NB = 10000;
void fibon(unsigned int size, unsigned int p[size]) {
for (unsigned int i = 0; i <= size; i++) // BUG: i < size
if (i < 2)
p[i] = i;
else
p[i] = p[i - 1] + p[i - 2]; // BUG: write p[NB]
}
int main() {
assert(NB > 2);
unsigned int *p = malloc(sizeof(int[NB]));
assert(p != NULL);
fibon(NB, p);
free(p);
return EXIT_SUCCESS;
}
Écrire loin au-delà de la zone allouée
modifierCréer un fichier bug_overwriteheap_large.c contenant le code suivant.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
const unsigned int SIZE = 10000; // size may be a misleading name
/**
fibon overwrite the array 4 times the length allocated by main
Thanks to the C11 array size notation, gcc-12+ detects the bug and
warns you ! Read the warnings ! Solve them !
Use as recent as possible compiler. Even for quite stable language
as C, it is helpful.
Note also that with the old classical notation, aka.
unsigned int size, unsigned int *p
or if you do not indicate the size of the array in the argument,
the compiler may not help you.
@param size size of the array
@param p array of size "size"
*/
void fibon(unsigned int size, unsigned int p[static size]) {
for (unsigned int i = 0; i <= size; i++)
if (i < 2)
p[i] = i;
else
p[i] = p[i - 1] + p[i - 2];
}
int main() {
assert(SIZE > 2);
unsigned int *p = malloc(SIZE); // BUG: 4x smaller than correct sizeof( int[SIZE] ));
assert(p != NULL);
fibon(SIZE, p);
free(p);
return 0;
}
Les consignes
modifierLire un peu avant la zone allouée
modifierLe code bug_overreadheap_tiny.c
lit une valeur juste un
peu devant l'allocation. Comme c'est la première allocation, elle est
en bordure de page et cela provoque instantanément un SEGFAULT.
Il est donc relativement aisé de trouver la source du
problème avec gdb en demandant simplement les valeurs des
variables.
- Compiler le fichier
bug_overreadheap_tiny.c
. Un compilateur récent devrait détecter le bug. - Lancer le programme
bug_overreadheap_tiny
. Vous devriez obtenir le SEGFAULT attendu - Lancer le programme avec Valgrind qui devrait vous indiquer la ligne du code avec l'erreur.
- Lancer le programme dans gdb. Afficher le contenu des 20 premières cases du tableau en décimal.
- Demander l'affichage de
p[i-2]
- Recompiler le programme avec ASan. Lancer le programme. ASan détecte bien le SEGFAULT mais ne vous indique rien sur sa cause.
Vous devriez avoir tapé les commandes suivantes (Certains affichages sont omis):
$ gcc -o bug_overreadheap_tiny bug_overreadheap_tiny.c -g -Wall -Wextra -fanalyzer
[... détection du bug par le compilateur ...]
$ ./bug_overreadheap_tiny
Erreur de segmentation (core dumped)
$ valgrind ./bug_overreadheap_tiny
[...]
==19345== Invalid read of size 4
==19345== at 0x1091BC: fibon (bug_overreadheap_tiny.c:27)
==19345== by 0x109276: main (bug_overreadheap_tiny.c:35)
==19345== Address 0x404a8003c is not stack'd, malloc'd or (recently) free'd
== 19345 ==
[...]
$ gdb ./bug_overreadheap_tiny
(gdb) layout src
(gdb) run
(gdb) x /20d p
(gdb) print p[i-2]
Cannot access memory at address 0x55595555929c
(gdb) quit # confirm
Écrire un peu après la zone allouée
modifierLe code bug_overwriteheap_tiny.c
écrit une
valeur juste un peu après l'allocation (un entier de 4 octets, 0 octet
après, donc juste après). Lors d'une exécution normale, il ne se passe
rien ! Il se termine parfaitement normalement. Mais le bug est bien
là. Valgrind est capable de le détecter.
- Compiler le fichier
bug_overwriteheap_tiny.c
. Il est fort possible que votre compilateur ne remonte aucun problème. - Lancer le programme. Il ne devrait y avoir aucun problème non plus, car la détection matérielle ne fonctionne qu'à la granularité d'une page (typiquement 4Kio).
- Lancer le programme avec Valgrind qui devrait vous indiquer la ligne du code avec l'erreur.
- Recompiler le programme avec ASan. Lancer le programme. L'erreur devrait aussi être détectée à l'exécution.
Vous devriez avoir tapé les commandes suivantes (Certains affichages sont omis):
$ gcc -o bug_overwriteheap_tiny bug_overreadheap_tiny.c -g -Wall -Wextra -fanalyzer
$ ./bug_overwriteheap_tiny
$ valgrind ./bug_overwriteheap_tiny
[...]
==19897== Invalid write of size 4
==19897== at 0x1091D2: fibon (bug_overwriteheap_tiny.c:27)
==19897== by 0x109276: main (bug_overwriteheap_tiny.c:36)
==19897== Address 0x4a89c80 is 0 bytes after a block of size 40,000 alloc'd
==19897== at 0x483F7B5: malloc (vg_replace_malloc.c:381)
==19897== by 0x109230: main (bug_overwriteheap_tiny.c:33)
== 19897 ==
==19897==
[...]
$ gcc -o bug_overwriteheap_tiny bug_overreadheap_tiny.c -g -Wall -Wextra -fsanitize=address
[... détection par ASan à l'exécution ...]
Écrire loin au-delà de la zone allouée
modifierDans le code bug_overwriteheap_large.c
l'allocation est 4 fois trop petite. Beaucoup d'écritures
débordent. Néanmoins, la détection n'a lieu qu'au moment du free.
Valgrind sait détecter le problème. Mais si nous soupçonnons un
débordement en écriture, nous pouvons demander à gdb de surveiller
explicitement des modifications juste après notre allocation.
Cette technique est particulièrement utile lorsque quelque chose a modifié une de vos structures de données: vous ne savez pas quoi ou quand, mais vous savez où.
- Compiler le fichier
bug_overwriteheap_large.c
. Le compilateur devrait détecter le problème. - Lancer le programme. La détection du problème a lieu au moment du free.
- Lancer le programme avec Valgrind qui devrait vous indiquer les lignes du code avec l'erreur.
- Charger le programme dans GDB. Mettre un breakpoint au niveau du malloc. Après l'exécution du malloc, mettre un watchpoint sur la mémoire juste après le malloc (attention à l'arithmétique de pointeurs !). Continuer.
- Recompiler le programme avec ASan. Lancer le programme. L'erreur devrait aussi être détectée à l'exécution.
Vous devriez avoir tapé les commandes suivantes (Certains affichages sont omis):
$ gcc -o bug_overwriteheap_large bug_overreadheap_large.c -g -Wall -Wextra -fanalyzer
$ ./bug_overwriteheap_large
double free or corruption (!prev)
$ valgrind ./bug_overwriteheap_large
[... détection des erreurs ...]
$ gdb ./bug_overwriteheap_large
(gdb) break 33
(gdb) run
(gdb) next
(gdb) print p
$1 = (unsigned int*) 0x5555555592a0
(gdb) watch *(int *)(0x5555555592a0 + SIZE)
Hardware watchpoint 2: *(int *)(0x5555555592a0 + SIZE)
(gdb) cont
(gdb) print i # les problèmes commencent à 1/4 de la longueur attendue
$2 = 2500
(gdb) print size
$3 = 10000
(gdb) quit # confirm
$ gcc -o bug_overwriteheap_large bug_overreadheap_large.c -g -Wall -Wextra -fsanitize=address
$ ./bug_overwriteheap_large
[... détection du bug ..]