GDB¶
Liste des commandes¶
gdb(1) permet de débugger vos programmes plus facilement en permettant d’analyser l’état durant l’exécution. Pour pouvoir analyser votre exécutable avec gdb(1), vous devez ajouter les symboles de débuggage lors de la compilation en utilisant l’option -g de gcc(1):
gcc -g gdb.c -o my_program
L’option -g de gcc(1) place dans l’exécutable les informations sur les noms de variables, mais aussi tout le code source.
Lancez gdb avec la commande gdb my_program. Ceci va vous ouvrir la console de gdb qui vous permet de lancer, le programme et de l’analyser. Pour démarrer le programme, tapez run. gdb va arrêter l’exécution au premier problème trouvé. Votre programme tourne encore pour l’instant. Arrêtez-le avec la commande kill.
Breakpoint¶
Pour analyser un programme, vous pouvez y placer des breakpoints. Un breakpoint permet de mettre en pause l’exécution d’un programme à un endroit donné pour pouvoir afficher l’état des variables et faire une exécution pas-à-pas. Pour mettre un breakpoint, vous avez plusieurs choix:
break [function]met en pause l’exécution à l’appel de la fonction passée en argument à la commandebreak [filename:linenumber]spécifie le fichier du code source et la ligne à laquelle l’exécution doit s’arrêterdelete [numberbreakpoint]supprime le breakpoint spécifiéNote : Chaque breakpoint est caractérisé par un numéro. Pour obtenir la liste des breakpoints utilisés
info break
Informations à extraire¶
Une fois un breakpoint placé, plusieurs informations peuvent être extraites via gdb(1) :
print [variablename]affiche la valeur de la variable dans son format de base. Il est possible de connaître la valeur pointée en utilisant*ainsi que l’adresse de la variable avec&.Il est aussi possible de modifier une variable avec ``set variable [nom_variable] = [valeur]``. De façon similaire avec ``print [nom_variable] = [valeur]``.
info reg [registre]affiche les informations sur tous les registres si aucun registre n’est explicitement spécifié.info reg eaxdonne le même résultat queprint $eax.Il est intéressant de noter qu'il est possible d'afficher une variable sous le format spécifié. Pour cela, remplacer ``print`` par : * ``p/x`` - affiche en format hexadécimal la variable spécifiée * ``p/d`` - en format entier signé * ``p/f`` - en format floating point * ``p/c`` - affiche un caractère.
backtraceoubtaffiche la pile des appels de fonctions.Il est possible de naviguer dans la pile des appels à l'aide de ``up`` et ``down``. Ces deux commandes montent et descendent respectivement dans la pile. C'est très utile car il est possible de modifier le contexte dans lequel on se trouve pour afficher les variables.
info framedonne des informations sur la frame actuelle.listaffiche les lignes de codes entourant le break. On peut donc facilement voir le code posant un problème ou analyser le code avant de faire une avancée pas à pas.show argsaffiche les arguments passés au programme.info breakpointsaffiche les breakpointsinfo diplaysaffiche les displaysinfo func [fonctionname]affiche le prototype d’une fonction
Avancement de l’exécution¶
Quand vous avez acquis suffisamment d’informations sur le programme, vous avez plusieurs choix pour continuer son exécution :
nextexécute la prochaine instruction de votre code source, mais sans rentrer dans des fonctions externes.stepexécute la prochaine instruction de votre code source, mais en entrant dans le code des fonctions appelées.continuecontinue le reste de l’exécution jusqu’au prochain breakpoint.
Automatisation¶
Lors d’un débuggage long et fastidieux, il est parfois nécessaire d’exécuter certaines commandes à chaque breakpoint.
commands [numerobreakpoint]définit une liste de commandes associées à un breakpoint. Celles ci seront exécutées quand on s’arrêtera sur ce breakpoint. Il suffit de taper les commandes à effectuer les unes après les autres et de terminer parend. Si vous ne fournissez pas de numéro, les commandes sont assignées au dernier breakpoint créé.display [variablename]affiche la variable à chaque breakpoint.
Gestion des Signaux¶
En plus des breakpoints, gdb(1) interrompt l’exécution du programme en cours lorsqu’il intercepte certains signaux d’erreurs comme les signaux SIGSEGV et SIGINT. gdb(1) permettra alors de corriger plus facilement certaines erreurs comme les erreurs de segmentation ou les problèmes de deadlocks.
Il est possible de gérer le comportement de gdb(1) lorsque des signaux sont interceptés. Tout d’abord, la commande info signals permet d’afficher la liste des signaux reconnus par gdb(1) ainsi que la façon dont il les traite (par exemple interrompre le programme en cours ou non). On peut changer la façon de traiter un signal avec la commande handle [SIGNAL] [HANDLING...] où [SIGNAL] est le signal à intercepter (son numéro ou son nom complet) et [HANDLING] la façon de traiter ce signal par gdb(1) [1]. Par exemple, la commande handle SIGALRM stop print permet d’interrompre le programme et d’afficher un message quand gdb intercepte le signal SIGALRM.
Localiser un signal¶
Avec gdb(1), il est possible de localiser un signal et de débugger certaines erreurs comme une erreur de segmentation. En effet, lorsque gdb(1) interrompt le programme en cours après l’interception d’un signal d’erreur comme SIGSEGV, il est possible de trouver la ligne du programme à laquelle le signal a été intercepté en tapant le mot-clé where une fois le programme interrompu (il est cependant nécessaire d’avoir compilé le programme avec l’option -g de gcc pour trouver la ligne précise). Ensuite, grâce aux commandes expliquées plus tôt, il est possible de vérifier les valeurs des variables lors de l’interception du signal pour trouver l’origine du problème.
En plus de localiser facilement les erreurs de segmentation dans un programme, vous pourrez analyser plus aisément les problèmes de deadlock des threads. En effet, lorsque le programme est lancé sur le shell et que vous remarquez un deadlock, vous pouvez appuyer sur CTRL + C pour lancer le signal SIGINT au programme. Cela permettra de trouver les endroits où bloquent les différents threads du programme à l’aide des commandes décrites dans la section de débuggage des threads ci-dessous.
Extraction de code assembleur¶
disasaffiche le code assembleurdisas /m blahmet en correspondance le code assembleur et le code source
Pour arrêter la console de gdb, tapez quit.
Illustration avec des exemples¶
Premier programme¶
Le premier programme est
src/calc.c. Compilez-le et exécutez le pour vous apercevoir que le programme est erroné. A priori vous avez peu, ou pas, d’informations sur l’erreur. Lancez donc gdb à l’aide degdb calcpuis lancez le programme avecrun.Program received signal SIGFPE, Arithmetic exception. => Exception arithmetique 0x0000000000400553 in calc (a=165, b=4) at calc.c:10 => Dans la fonction calc du fichier calc.c à la ligne 10 10 res = (a*5 -10) / (b-i); => Affichage de la ligne problématiqueLe premier réflexe doit être
listpour observer le code. Puisque le problème vient de la ligne 10 dans la boucle, nous allons nous arrêter à la ligne 10 avecbreak 10et relancer le programme. Le programme va s’arrêter avant le début de la boucle. Utilisezprint aetprint bpour connaître les arguments reçus par calc.Il est intéressant de noter une particularité du language C par rapport à java : une variable déclarée n'est pas initialisée à 0 par défaut, elle reprend juste la valeur de la mémoire avant son affectation. ``print i`` et ``print res`` vous donneront donc des résultats aléatoires.Puisque le problème vient du calcul arithmétique, placez un break sur cette ligne pour pouvoir observer à chaque itération les variables.
break 9puiscommandsqui permet d’automatiser des commandes. Nous rajouterons comme commandes :* ``echo i : `` * ``print i`` * ``echo b : `` * ``print b`` * ``echo numerateur : `` * ``print a*5 -10`` * ``echo denominateur : `` * ``print b-i`` * et enfin ``end`` pour terminer la liste de commandes.Il ne reste plus qu’à avancer avec
continuepour aller de breakpoint en breakpoint et d’observer les variables pour comprendre le problème. On va pouvoir deviner que le problème vient d’un dénominateur nul. Pour résoudre ce problème, il faut passer une valeur plus grande que 6 à calc lors de son appel depuis la fonction main.list mainsuivi de plusieurslistpermet de visualiser la main. On peut repérer l’appel de la fonction calc à la ligne 18.Supprimez les anciens break avec
delete [numerobreakpoint]le numéro du breakpoint est connu viainfo break. Rajoutez un break à la ligne 18,break 18et lancez le programme.set variable m = 10pour assigner la valeur 10 à la variable m. Puis continuez l’exécution du programme. Celui se terminera normalement puisque il n’y a plus de division par zéro.
Deuxième programme¶
Le deuxième programme est appelé
src/recursive.c. Celui ne présente aucun bug et se déroulera normalement. Toutefois, il est intéressant d’utiliser gdb(1) pour bien comprendre les différents contextes au sein d’un programme. Mettez un break sur la fonctionfactTmp avecbreak factTmpet ajoutez automatiquement à ce breakpoint la commandebacktrace, viacommands. Ensuite, lancez le programme.backtracevous permet de visualiser les appels de fonction effectués. Nous pouvons voir que la fonctionfactTmpa été appelée parfactTerminal, elle même appelée par la fonctionmain.#0 factTmp (acc=1, nbr=6) at recursive.c:8 #1 0x000000000040057d in factTerminal (a=6) at recursive.c:17 #2 0x0000000000400598 in main (argc=1, argv=0x7fffffffe1b8) at recursive.c:23Essayez d’afficher les variable
globalVarpuislocalVar. Vous remarquerez qu’il n’est pas possible d’afficherlocalVarpuisque cette variable ne fait pas partie de l’environnement contextuel de factTmp. Pour afficher cette variable, il faut remonter la liste des appels.uppermettra de remonter les appels pour pouvoir afficherlocalVar. Une fois la variable affichée, redescendez avecdownet continuez 4 fois le programme après le breakpoint. Vous remarquerez que la liste des appels s’allonge à chaque appel récursif, ce qui est tout à fait normal.Naviguez dans les appels récursifs de factTmp en affichant les valeur de
globalTmp,tmp,accetnbr. Il est important de bien comprendre que la variable statiqueglobalTmpest commune à tous les appels de la fonctionfactTmpet un changement de cette variable dans un des appels récursifs modifie la variable des autres appels. A contrario, la variable local ainsi que les arguments sont propres à chaque appel.Vous pouvez maintenant terminer le programme.
Troisième programme¶
Le troisième programme est
src/tab.c. Compilez-le. Ce programme s’exécute correctement, et pourtant, il y contient une erreur. Lancez le programme avec gdb et mettez un breakpoint sur la première instruction, à savoir la ligne 9. Pour comprendre un problème sans savoir où commencer, il est utile de suivre l’évolution des variables.Il est important de savoir que ``print``, ainsi que ``display``, supportent les expressions telles que : * tab[1], tab[i],... * &i, *i,...Avancez instruction par instruction, avec
stepounextet portez attention aux valeurs de tab[i] par rapport à i. Une fois le problème trouvé avec gdb, solutionnez le.
Plus d’informations sur gdb(1) peuvent être trouvées sur:
Débuggage des threads avec GDB¶
gdb(1) est aussi utile pour débugger des programmes avec des threads. Il permet de faire les opérations suivantes sur les threads:
- Recevoir une notification lors de la création d’un nouveau thread.
- Afficher la liste complète des threads avec
info threads.- Placer un breakpoint dans un thread. En effet, si vous placez un breakpoint dans une certaine fonction, et un thread passe lors de son exécution à travers ce breakpoint,
gdbva mettre l’exécution de tous les threads en pause et changer le contexte de la console gdb(1) vers ce thread.- Lorsque les threads sont en pause, vous pouvez manuellement donner la main à un thread en faisant
thread [thread_no]avecthread_noétant l’indice du thread comme indiqué parinfo threads
- D’autres commandes pour utiliser gdb(1) avec les threads:
Footnotes
| [1] | Une liste plus complète des mots-clés utilisables pour modifier le comportement de gestion des signaux peut-être consultée ici : ftp://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_38.html . |