GDB est le débogeur GNU (Gnu DeBugger). Il permet de trouver les bogues des programmes en C/C++ ou en assembleur, et plus généralement de voir le déroulement d'un programme quand il s'exécute.
GDB sait déboguer les exécutables écrits en C, C++ ou assembleur . Pour donner des informations intéressantes, GDB utilise les symboles qu'il trouve dans les exécutables. Ces symboles sont générés par le compilateur... à condition de lui dire de le faire !
GDB est le débogeur GNU (Gnu DeBugger). Il permet de trouver les bogues des programmes en C/C++ ou en assembleur, et plus généralement de voir le déroulement d'un programme quand il s'exécute.
Les commandes les plus utilisées de GDB sont :
run
break
where et bt
step et next
display et print
help
kill
quit (utile...)
et dans une moindre mesure file et core.
Qu'est-ce qu'on débogue ?
Comme mentionné plus haut, GDB sait déboguer les exécutables écrits en C, C++ ou assembleur (cf. cours de logiciels de base). Pour donner des informations intéressantes, GDB utilise les symboles qu'il trouve dans les exécutables. Ces symboles sont générés par le compilateur... à condition de lui dire de le faire !
Le compilateur est generalement utilise est gcc. L'option à donner est -g (ou éventuellement -ggdb), à la fois à la production des fichiers objets (.o) et à l'édition de liens (production de l'exécutable proprement dit).
Les commandes
Lancer GDB
Tout d'abord, voyons comment lancer GDB. Il y a trois manières de le faire :
gdb (sans argument)
gdb
gdb
la plus usuelle étant la deuxième.
Le fichier à déboguer est un exécutable ; en ce qui concerne le fichier core, nous y reviendrons plus tard. Ces commandes lancent GDB. Pour les deux premières versions de la ligne de commande, GDB affiche un message de bienvenue puis l'invite qui est (gdb).
Pour la suite, nous supposerons que le fichier à déboguer s'appelle bugger et qu'il est le résultat de la compilation de bugger.c.
Les commandes de gdb
La permière chose à faire est de donner à manger à ce pauvre GDB, sans quoi il serait bien en peine de trouver des bogues. Si vous n'avez pas spécifié de fichier à déboguer sur la ligne de commande lançant GDB, il est temps de taper :
file bugger
ce qui a pour effet de charger le fichier code en mémoire. Si tout se passe bien, GDB répond :
Reading symbols from bugger...done.
On va enfin pouvoir faire des choses...
Trouver où est le bug
Quand on n'a aucune idée de l'endroit où est le méchant bogue, on laisse travailler GDB. Ou plutôt, on le met au travail, parce que, c'est pas pour dire, mais il n'a pas fait grand chose pour le moment, le bougre.
On utilise la commande :
run
Si le programme a besoin d'arguments, on les tape après le run, comme si on tapait bugger à partir du shell.
GDB lance le programme, qui s'exécute normalement jusqu'à ce que le bug remonte à la surface. Si votre programme fait des lectures au clavier ou des sorties à l'écran, GDB les fera également. Tout se passe comme en ligne de commande, jusqu'à la terminaison du programme, qui peut être :
normale (GDB dit Program exited normally.) ;
anormale (GDB dit Program exited abnormally., ce qui est souvent provoqué par la commande abort du C) ;
avec un code de retour (GDB dit Program exited with code XX., ce qui est provoqué par la commande exit(XX) ;
avec un signal, le plus souvent SIGSEGV (GDB dit Program received signal SIGSEGV, Segmentation fault.), suivi d'informations exploitables.
Au point où on en est, il est temps de travailler sur un exemple concret. Prenons le programme suivant :
#include
#include
char *lire_fichier(FILE* f){
int c,i=0;
char* str;
while ((c=getc(f))!=EOF){
str[i++]=(char)c;
}
str[i]=' ';
return str;
}
int main(int argc, char **argv){
char *chaine;
FILE* file;
file=fopen(argv[1],"r");
chaine=lire_fichier(file);
printf("Contenu du fichier:
%s
",chaine);
return 0;
}
Deux mots sur le programme. Il ouvre le fichier dont le nom est passé en argument, la fonction lire_fichier lit le contenu du fichier f et le met dans la chaîne str.
Le bug, c'est (évidemment) qu'aucune mémoire n'est allouée pour le pointeur str. C'est une erreur classique, et qui permet de bien voir les fonctionnalités de GDB, parce que, après tout, on est là pour ça.
On reprend à zéro. L'invite de mon shell est $ et je commence par compiler le programme :
$ gcc -g -o bugger bugger.c
Puis, lançons GDB :
$ gdb
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "sparc-sun-solaris2.8".
(gdb) file bugger
Reading symbols from bugger...done.
(gdb) run /etc/hosts
Starting program: /users3/bchastag/bugger /etc/hosts
Program received signal SIGSEGV, Segmentation fault.
0x1067c in lire_fichier (f=0xff33c304) at bugger.c:8
8 str[i++]=(char)c;
(gdb)
On a déjà beaucoup d'informations. Décortiquons la ligne qui suit Segmentation fault.
0x1067c, c'est l'adresse de l'instruction qui a tout fait planter. En général, c'est assez illisible, alors on l'oublie.
in lire_fichier désigne la fonction qui contient le vilain bogue.
f=0xff33c304 est la valeur du paramètre de la fonction lire_fichier. Ici, c'est un sale pointeur assez incompréhensible, mais cela peut déjà donner des informations. Par exemple si la valeur est 0x0, c'est un pointeur nul, et c'est louche.
at bugger.c:8 est le nom du fichier source qui contient la fonction, et le numéro de la ligne où est apparu le bogue, ici la ligne 8. La ligne suivante est carrément la ligne de code qui est fautive.
Maintenant, on a de vraies informations sur l'erreur. Normalement, quand on voit que c'est une affectation qui plante, on doit tout de suite penser que c'est un problème de mémoire pas allouée, mais bon, pour la démonstration, on va continuer à osculter cet exemple. Mais comment est-on arrivé là ?
C'est bien beau de savoir quelle ligne provoque le Segmentation Fault, mais ce n'est pas forcément là qu'est le bogue.
Première question à se poser : d'où vient-on ? Pour le savoir, on utilise la commande bt (pour "backtrace") ou where :
(gdb) where
#0 0x1067c in lire_fichier (f=0xff33c304) at bugger.c:8
#1 0x106f4 in main (argc=2, argv=0xffbefb94) at bugger.c:18
(gdb)
qui affiche la pile des appels de fonctions. Le format est le même que précédemment, à cela près que chaque ligne est précédée de #numéro. Nous verrons ce que cela signifie plus tard.
Ceci apporte surtout qu'on sait comment on est arrivé à la ligne contenant le bogue. Ici, on sait que la fonction main appelle lire_fichier à la ligne 18 et que le bogue apparaît ligne 8. Et les variables dans tout ça ?
Maintenant, il serait intéressant de savoir les valeurs des variables. La commande à utiliser est :
display
ou aussi :
print
La différence entre les deux fonctions est que display fait l'affichage après chaque commande tandis que print ne fait qu'un seul affichage.
Nous avons trois variables : c, i et str. Demandons gentiment à GDB de nous dire leur valeur :
(gdb) display i
1: i = 0
(gdb) display c
2: c = 35
(gdb) display str
3: str = 0x10034 ""
(gdb)
Cela est bien, mais str est un tableau, donc ce serait plus intéressant de connaître la valeur de son contenu. Rien de plus facile :
(gdb) display str[i]
4: str[i] = Cannot access memory at address 0x0.
Disabling display 4 to avoid infinite recursion.
(gdb)
Ce message d'erreur est bien clair : il y a un problème avec la variable str. Il faut noter que cet affichage dépend du système sur lequel on travaille : chez moi, je n'ai pas cette erreur, mais str[i] donne un affichage (un peu débile, certes). Si la variable est "normale", ça marche. Et maintenant, qu'est-ce qu'on fait ?
Tout ceci est bien, mais ce serait encore mieux si on pouvait suivre le déroulement du programme dans tous ses détails. On va donc relancer le programme et faire du pas à pas où on veut.
Tout d'abord, il faut arrêter notre programme. Bien qu'il soit planté, GDB considère que le programme tourne encore (pour afficher les variables, la pile...). Pour arrêter le programme, on fait :
kill
.
(gdb) kill Kill the program being debugged ? (y or n) y (gdb)
Avant tout, on va placer un point d'arrêt au début de la fonction qui fait planter (le reste, on s'en fiche). Pour cela, on utilise la commande :
break
(gdb) break 4
Breakpoint 1 at 0x105ec: file bugger.c, line 4.
(gdb)
Note : si on débogue un programme utilisant plusieurs fichiers source, il faut préciser le fichier. On taperait alors :
break bugger.c:4
On peut aussi donner un nom de fonction :
break lire_fichier
Maintenant, on relance le programme, qui va s'arrêter au premier point d'arrêt trouvé (notons que GDB reprend automatiquement les arguments du précédent appel à run lorsqu'on utilise run sans arguments.) :
(gdb) run
Starting program: /users3/bchastag/bugger bugger /etc/hosts
Breakpoint 1, lire_fichier (f=0x3) at bugger.c:4
4 char *lire_fichier(FILE* f){
3: str = 0x10034 ""
2: c = 2010
1: i = -12906352
(gdb)
Remarquons au passage que les variables dont nous avons demandé l'affichage à l'aide de display sont bien affichées automatiquement après chaque commande. Faisons du pas à pas. La commande est :
next
Là, il faut apporter quelques précisions. La commande next exécute les instructions les une après les autres, jusque là rien de compliqué. Et les appels de fonctions ? Et bien, la commande next les considère comme une seule instruction, et exécute donc son code sans rien détailler.
Si on veut entrer dans la fonction avec le débogueur pas à pas, il faut utiliser la commande step. Il en va de même pour les boucles (while et for).
Voyons maintenant le résultat de next jusqu'à l'erreur (notons que quand on ne tape que sur entrée, GDB exécute la commande précédente) :
(gdb) next
lire_fichier (f=0x8049708) at bugger.c:5
5 int c,i=0;
3: str = 0x10034 ""
2: c = 2010
1: i = -12906352
(gdb)
7 while ((c=getc(f))!=EOF){
3: str = 0x10034 ""
2: c = 2010
1: i = 0
(gdb)
8 str[i++]=(char)c;
3: str = 0x10034 ""
2: c = 35
1: i = 0
(gdb)
Program received signal SIGSEGV, Segmentation fault.
0x080484cb in lire_fichier (f=0xff33c304) at bugger.c:8
8 str[i++]=(char)c;
3: str = 0x10034 ""
2: c = 35
1: i = 0
(gdb)
Il faut noter qu'en fonctionnement normal (hors plantage), la ligne de code affichée par GDB est celle qui va s'exécuter. Elle n'a donc pas encore été exécutée.
Quelques commandes sont associées à step et next :
ni et si : comme next et step mais peuvent prendre un argument N pour exécuter N instructions d'un coup ;
cont : reprend l'exécution du programme jusqu'au prochain point d'arrêt.
Pour quitter GDB, la commande est tout simplement quit. Si votre programme est toujours en train de tourner, GDB vous demande confirmation pour quitter. Pour éviter cela, il suffit de penser à tuer son processus (commande kill) avant de quitter.
Pour aller plus loin
Quelques commandes utiles
Une commande très utile de GDB est help. Elle affiche la liste des catégories de commandes. Un help commande donne davantage d'informations sur la commande. A utiliser sans modération.
Une autre commande utile est info, suivi ou non d'un nom de commande, par exemple info threads affichera les informations concernants les threads courants.
GDB connaît la commande make, très utile pour reconstruire un exécutable sans avoir à quitter puis relancer GDB
Enfin, GDB connaît la complétion automatique pour les noms de fichers : taper TAB affichera toutes les possibilités, comme dans un shell.
Sélectionner un bloc de mémoire
Quand on a des appels de fonctions compliqués, il n'est pas rare que l'on veuille des informations non pas sur le bloc de mémoire courant, mais sur un autre (un appelant). C'est souvent le cas quand on a un malloc qui plante : GDB affiche fièrement le numéro de ligne fautif de malloc.c, mais on s'en fiche.
Rappelons la trace pour notre petit exemple :
(gdb) where
#0 0x1067c in lire_fichier (f=0xff33c304) at bugger.c:8
#1 0x106f4 in main (argc=2, argv=0xffbefb94) at bugger.c:18
(gdb)
Les seules variables que l'on puisse afficher ici sont celles visibles par la fonction lire_fichier. Pour accéder aux variables du main, il faut taper la commande suivante :
frame
(gdb) frame 1
#1 0x106f4 in main (argc=2, argv=0xffbefb94) at bugger.c:18
18 chaine=lire_fichier(file);
(gdb)
Voilà à quoi servent les fameux numéros que nous donnaient la commande where. Et maintenant, on peut taper print chaine sans problème.
A quoi servent ces fichiers core ?
Quand un programme plante, il n'est pas rare qu'un fichier core soit créé. Ce fichier contient l'état de la mémoire au moment du plantage (ce qui explique qu'ils soient si gros).
Dans certains cas, le bogue n'est pas déterministe. Dans d'autres cas, il y a un bogue en fonctionnement normal, mais il disparaît quand on met des traces ou quand on utilise GDB. Ces deux derniers cas sont dûs au fait que la fonction printf et GDB modifient le tas (heap), et donc le programme obtenu n'est plus celui qui plante.
Dans ces cas, le fichier core va nous aider. Comme il contient l'état du système au moment du plantage, on devrait pouvoir comprendre d'où vient le ploblème.
Reprenons notre exemple, et lançons GDB :
$ gdb bugger
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "sparc-sun-solaris2.8"...
(gdb) core core
warning: core file may not match specified executable file.
Core was generated by `./bugger /etc/hosts'.
Program terminated with signal 11, Segmentation Fault.
Reading symbols from /usr/lib/libc.so.1...done.
Reading symbols from /usr/lib/libdl.so.1...done.
Reading symbols from /usr/platform/SUNW,Sun-Fire/lib/libc_psr.so.1...done.
#0 0x1067c in lire_fichier (f=0xff33c304) at bugger.c:8
8 str[i++]=(char)c;
(gdb)
La commande core core dit qu'on va utiliser le fichier de type core qui s'appelle "core".
On constate que le résultat est le même que celui obtenu par un run. Magique...
Cependant, les bogues nécessitant l'usage du fichier core... je n'en n'ai jamais vu. Ces fichiers sont assez gourmands en espace mémoire (80 Ko pour cet exemple basique, mais j'ai déjà vu des fichiers core de 2 Go), mieux vaut donc ne pas les garder. Pour cela, plusieurs moyens :
les enlever régulièrement à la main. Pas très pratique, ni très sûr (un oubli est vite arrivé) ;
limiter leur taille au niveau du shell en ajoutant au .tcshrc (respectivement le .bashrc) la ligne suivante :
limit coredumpsize 0
(ou, en bash : ulimit -c 0) ;
ajouter à la fin de votre .xsession la commande :
find -name 'core' -exec /bin/rm -f {} ;
Le mieux étant d'utiliser les deux dernières solutions ensemble.
Pour plus de documentation
La documentation complète de GDB peut être trouvée sur le site de GNU à l'adresse suivante : http://www.gnu.org/manual/gdb-5.1.1
Extract from ensigogne : writted by laurent.goujon@ensimag.fr
|