Micro-tuto IsiLabs

Débugger son code C plus efficacement avec GDB

D'abord, quelques ressources

Note : il n'est pas nécessaire d'utiliser GCC, Clang par exemple implémente plus ou moins les mêmes options

Pour bien commencer

Compiler le code

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% █
Un warning ? Bof, c'est pas une erreur, pas grave

Petits tests ?

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

En fait, non

issotm@sheik-kitty gdb% ./shifter
zsh: segmentation fault (core dumped)  ./shifter
139 issotm@sheik-kitty gdb% █
(Le résultat varie de machine en machine ; il est possible que le programme boucle indéfiniment, par exemple)

Plus rien ne va
Mais comment le résoudre ?

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 !

Bases de 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 est possible que vous n'ayiez pas de couleur avec des vieilles versions)

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.

Première commande : 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) █
C'est à Run qu'on passe les arguments du programme.

Deuxième commande : Quit

(gdb) quit
issotm@sheik-kitty gdb% █
Si le programme est en cours d'exécution, une confirmation sera demandée avant de quitter.

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) █
(Les noms et l'adresse mémoire des fonctions peuvent être différents selon les machines)

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) █
On manque d'informations..!

La pêche aux infos

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.

Deuxième prise !

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) █
Notez qu'on n'a plus (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);
            
La backtrace indique que l'appel est à la ligne 44 de shifter.c

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% █
Nickel, ça crashe plus !

Jamais deux sans trois

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% █
Pas encore prêts pour l'international...
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) █
Ce coup-ci, l'erreur vient de 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.

Découvrons la commande Print !
(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);
            
Vous pourriez mettre un message d'erreur, si vous préférez.
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.)

2 puissance 32..?

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% █
Le résultat peut être différent selon votre machine !

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 aller plus loin

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)

Commandes gdb intéressantes

Help
Fournit de l'aide sur une commande
APRopos
Fournit une liste de commandes "à propos" d'un certain terme
Info
Affiche différentes infos sur l'exécution du programme
X
Permet d'examiner la mémoire, pratique pour les tableaux
DISPlay
Permet d'afficher une valeur automatiquement
LIst
Affiche les lignes de code C autour de la ligne courante
Break
Place un breakpoint quelque part
START
Place un breakpoint sur main et run
Delete
Retire un breakpoint
Next
Avance le programme jusqu'à la prochaine ligne (ne rentre pas dans les fonctions)
Step
Avance le programme jusqu'à la prochaine ligne (rentre dans les fonctions)
Continue
Rend la main au programme
DISASsemble
Si vous voulez voir les instructions que le processeur est en train d'exécuter...
PEDA
Python Exploit Development Assistant. Ce n'est pas exactement une commande, mais une extension.

Merci d'avoir suivi !

Plus d'Isilabs :