Débugger son code C plus efficacement avec GDB
Note : il n'est pas nécessaire d'utiliser GCC, Clang par exemple implémente plus ou moins les mêmes options
issotm@sheik-kitty gdb% make cc -O2 -Wall -Wextra -o shifter shifter.c shifter.c: Dans la fonction « main »: shifter.c:38:14: attention: paramères « argc » inutilisé [-Wunused-parameter] 38 | int main(int argc, char * argv[]) { | ~~~~^~~~ issotm@sheik-kitty gdb% █
issotm@sheik-kitty gdb% ./shifter en Please enter an integer N: 31 2^N = 2147483648 issotm@sheik-kitty gdb% ./shifter fr Veuillez entrer un entier N : 4 2^N = 16 issotm@sheik-kitty gdb% █
Chouette, tout baigne :D
issotm@sheik-kitty gdb% ./shifter zsh: segmentation fault (core dumped) ./shifter 139 issotm@sheik-kitty gdb% █
Première étape : trouver le bug
I will look for you, I will find you...
Solution intuitive : printf
!
À proscrire !
Le mot-clef est undefined behavior. En gros, dans la plupart des cas, le compilateur est libre de réarranger le code d'une manière qui fausse les observations.
Pour plus d'informations, voir la partie 3 des articles de John Regehr sur l'UB, particulièrement la section "Another Example".
...and I will kill you.
On va donc utiliser une autre solution, plus efficace, précise et puissante : gdb !
Lancer son programme :
issotm@sheik-kitty gdb% gdb ./shifter GNU gdb (GDB) 8.3 Copyright (C) 2019 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 WWARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-pc-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 ./shifter... (No debugging symbols found in ./shifter) (gdb) █
Il ne faut pas passer les arguments du programme à gdb, uniquement le chemin vers l'exécutable !
(Aussi, "No debugging symbols" ? Probablement rien de grave...)
Vous pouvez passer -q
à GDB pour qu'il n'affiche pas le paragraphe d'aide au lancement.
Le petit prompt (gdb)
indique qu'il accepte des commandes.
Si le programme a actuellement la main, Ctrl+C l'interrompt et gdb prend la main.
À noter que les commandes GDB peuvent être abrégées, les lettres essentielles seront en majuscules par la suite.
Run
(gdb) run fr Starting program: /home/issotm/hacklab/micro-tutos/gdb/shifter fr Veuillez entrer un entier N : 4 2^N = 16 [Inferior 1 (process 83709) exited normally] (gdb) █
Run
qu'on passe les arguments du programme.Quit
(gdb) quit issotm@sheik-kitty gdb% █
Les plus pressés utiliseront probablement juste Ctrl+D
Sarah Connor ?
Tentons de run
avec aucun argument...
(gdb) r Starting program: /home/issotm/hacklab/micro-tutos/gdb/shifter Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7f36162 in __strcmp_avx2 () from /usr/lib/libc.so.6 (gdb) █
Le programme a crashé et gdb a repris la main, mais là où le programme a crashé !
On va essayer une nouvelle commande : BackTrace
.
(gdb) bt #0 0x00007ffff7f36162 in __strcmp_avx2 () from /usr/lib/libc.so.6 #1 0x0000555555555220 in chaine_langue () #2 0x00005555555550a4 in main () (gdb) █
On voit la pile d'exécution : quelles fonctions ont appelé celle en cours.
L'erreur s'est produite dans une fonction de la librairie standard (strcmp
), donc soit on a trouvé un bug dans la libc (😂) soit on lui a passé un mauvais argument.
On souhaite donc examiner les arguments passés à strcmp
, on a donc besoin de se placer dans le contexte de la fonction appelante (chaine_langue
).
(gdb) bt #0 0x00007ffff7f36162 in __strcmp_avx2 () from /usr/lib/libc.so.6 #1 0x0000555555555220 in chaine_langue () #2 0x00005555555550a4 in main () (gdb) █
On va utiliser la commande FRame pour sélectionner une stack frame. Utilisez le numéro à gauche de la ligne de chaine_langue
.
(gdb) fr 1 #1 0x0000555555555220 in chaine_langue () (gdb) █
En soi, un programme ne contient peu ou pas d'informations sur son code source, c'est le but de la compilation. Mais on en aimerait bien pour débugger...
C'est l'intérêt de l'option -g
de GCC !
On va également se heurter au problème de l'optimisation : le compilateur est libre, si on l'autorise (option -O
), de réarranger le code pour le rendre plus efficace, tant que le programme se comporte toujours pareil.
Mais, pour débugger, ça va poser problème. On va donc désactiver les optimisations.
J'ai créé un target Make appelé develop
qui compile avec les options qui vont bien. Il faut juste faire make clean
avant, comme on veut tout recompiler.
(Si vous avez entendu parler de "configuration debug" versus "configuration release", c'est de ça que ça parle.)
issotm@sheik-kitty gdb% make develop cc -g -O0 -Wall -Wextra -o shifter shifter.c shifter.c: Dans la fonction « main »: shifter.c:38:14: attention: paramères « argc » inutilisé [-Wunused-parameter] 38 | int main(int argc, char * argv[]) { | ~~~~^~~~ issotm@sheik-kitty gdb% gdb -q ./shifter Reading symbols from ./shifter... (gdb) r Starting program: /home/issotm/hacklab/micro-tutos/gdb/shifter Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7f36162 in __strcmp_avx2 () from /usr/lib/libc.so.6 (gdb) bt #0 0x00007ffff7f36162 in __strcmp_avx2 () from /usr/lib/libc.so.6 #1 0x00005555555551bf in chaine_langue (chaines=0x555555558060 <prompts>, chaine_langue=0x0) at shifter.c:30 #2 0x000055555555522b in main (argc=1, argv=0x7fffffffe708) at shifter.c:41 (gdb) fr 1 #1 0x00005555555551bf in chaine_langue (chaines=0x555555558060 <prompts>, chaine_langue=0x0) at shifter.c:30 30 if (!strcmp(chaines_languages[langue], chaine_langue)) { (gdb) █
(No debugging symbols found in ./shifter)
!On voit directement que l'argument chaine_langue
vaut 0x0 (c'est-à-dire NULL
) ! C'est donc la fonction appelante qui pose un problème...
unsigned n;
printf("%s\n", chaine_langue(prompts, argv[1]));
scanf("%u", &n);
Ah bah oui, on a pas vérifié le nombre d'arguments !
unsigned n;
if (argc < 2) {
return 1;
}
printf("%s\n", chaine_langue(prompts, argv[1]));
scanf("%u", &n);
issotm@sheik-kitty gdb% make develop cc -g -O0 -Wall -Wextra -o shifter shifter.c issotm@sheik-kitty gdb% ./shifter 1 issotm@sheik-kitty gdb% █
On va débugger un crash un poil moins simple.
issotm@sheik-kitty gdb% ./shifter es zsh: segmentation fault (core dumped) ./shifter es 139 issotm@sheik-kitty gdb% █
issotm@sheik-kitty gdb% gdb -q ./shifter Reading symbols from ./shifter... (gdb) r es Starting program: /home/issotm/hacklab/micro-tutos/gdb/shifter es Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7f3ac55 in __strlen_avx2 () from /usr/lib/libc.so.6 (gdb) bt #0 0x00007ffff7f3ac55 in __strlen_avx2 () from /usr/lib/libc.so.6 #1 0x00007ffff7e50ac6 in puts () from /usr/lib/libc.so.6 #2 0x0000555555555240 in main (argc=1, argv=0x7fffffffe6f8) at shifter.c:45 (gdb) fr 2 #2 0x0000555555555240 in main (argc=1, argv=0x7fffffffe6f8) at shifter.c:45 45 printf("%s\n", chaine_langue(prompts, argv[1])); (gdb) █
main
!C'est normal que la backtrace indique puts
, faites comme si c'était printf
.
Question : d'où vient l'erreur ?
On sait que c'est un des arguments passés, donc c'est probablement le retour de chaine_langue
.
(gdb) p chaine_langue(prompts, argv[1])
$1 = 0x0
(gdb) █
Ah oui, la fonction peut retourner NULL
.
unsigned n;
char const * prompt;
if (argc < 2) {
return 1;
}
prompt = chaine_langue(prompts, argv[1]);
if (!prompt) {
return 1;
}
printf("%s\n", prompt);
scanf("%u", &n);
issotm@sheik-kitty gdb% make develop cc -g -O0 -Wall -Wextra -o shifter shifter.c issotm@sheik-kitty gdb% ./shifter es 1 issotm@sheik-kitty gdb% █
Dernière chose, le programme ne vérifie pas qu'on a rentré un nombre dans le scanf
, mais on ne cherchera pas à le corriger ici.
Vous pouvez vous amuser à essayer de régler ce problème par vous-mêmes ! Il y en a même beaucoup d'autres :)
(Indice : regardez le code de retour des différentes fonctions qu'on appelle.)
On va étudier (mais pas fixer) un dernier bug plus pernicieux. À votre avis, combien vaut 2 puissance 32 ?
issotm@sheik-kitty gdb% ./shifter fr Veuillez entrer un entier N : 32 2^N = 1 issotm@sheik-kitty gdb% █
Ce genre de bug provient en général d'une erreur de logique, mais pourtant on n'en a pas ici ?
On touche là à l'un des undefined behavior dont on parlait plus tôt.
Le compilateur produit souvent des warnings en cas d'undefined behavior, mais par exemple ici il n'y en a pas. Nous allons voir un outil qui permet d'en détecter à l'exécution.
Décommenter les deux flags à la ligne 10 du Makefile, puis make clean
et make develop
.
.PHONY:all
develop: all
develop: CFLAGS := -g -O0 -fsanitize=address -fsanitize=undefined
.PHONY: develop
clean:
issotm@sheik-kitty gdb% make clean rm -f shifter issotm@sheik-kitty gdb% make develop cc -g -O0 -fsanitize=address -fsanitize=undefined -Wall -Wextra -o shifter shifter.c issotm@sheik-kitty gdb% █
Maintenant, si on réexécute le programme avec comme argument 32...
issotm@sheik-kitty gdb% ./shifter fr Veuillez entrer un entier N : 4 2^N = 16 issotm@sheik-kitty gdb% ./shifter fr Veuillez entrer un entier N : 32 shifter.c:54:28: runtime error: shift exponent 32 is too large for 32-bit type 'int' 2^N = 1 issotm@sheik-kitty gdb% █
Comment corriger ? À vous de voir.
Pour en savoir plus sur l'undefined behavior, je vous recommande de vous renseigner sur cppreference.
Les différentes pages comportent des descriptions de ce qui est — ou pas — UB (exemple : les shifts).
La documentation de GCC sur leurs implementation-defined behavior se trouve ici pour la dernière version de GCC.
Si vous voulez en savoir plus sur des options comme -fsanitize
, lisez man gcc
ou bien la documentation en ligne (sélectionnez le premier lien de la version que vous avez)
Help
APRopos
Info
X
DISPlay
LIst
Break
START
main
et run
Delete
Next
Step
Continue
DISASsemble
PEDA