Introduction au C

Le logo du C
SommaireTrouver, comprendre et réparer les bugs

Utiliser GDB

GDB (GNU debbuger) est une commande très utile pour analyser les bugs dans ses programmes C.

Si vous avez un bug et que vous voulez vous servir de GDB pour l'éliminer, la première étape est de rejouter l'option -g à votre commande de compilation Clang (ce qui peut être fait avec la variable CFLAGS si vous utilisez un Makefile). clang truc.c -o truc devient donc clang -g truc.c -o truc (l'ordre n'a pas d'importance, on peut rajouter le -g à la fin aussi).

Cette option compile votre programme en mode « débogage » (mais ce mot est vraiment moche alors je vais juste dire debug ou debugging à partir de maintenant, même si l'Académie française désapprouverait). Ça veut dire que Clang va rajouter plein d'information au fichier binaire qu'il génère, que GDB pourra utiliser pour re-traduire le binaire en C, et ainsi afficher le vrai nom de nos fonctions et notre code en C quand on debug, à la place de binaire illisible.

Ensuite, lancez GDB avec comme argument le nom du programme à debugger. Par exemple, si on a un fichier exécutable truc, on lance GDB avec gdb truc. Un long message s'affiche, et on se retrouve dans quelque chose qui ressemble un peu à Bash : on peut taper des commandes, les exécuter avec Entrée, et leur résultat s'affiche. La différence c'est que les commandes qu'on va pouvoir taper ici vont nous servir à debugger notre programme.

La première commande à connaître est sans doute run, qui lance notre programme. Une autre commande très utile est quit pour fermer GDB, et retourner dans Bash (si on vous demande si vous voulez vraiment quitter, entrez yyes ») et validez).

Comment vraiment débugger

L'idée du débuggage est de s'arrêter à un endroit du programme et de pouvoir lire la valeur de différentes variables, pour mieux comprendre ce qui se passe aux différents moments de notre programme.

Pour s'arrêter, il y deux choix :

Dans ce cas, il faudra utiliser la commande break FICHIER:LIGNE (de préférence avant run). FICHIER est le nom du fichier .c qui contient le code que vous voulez débugger, et LIGNE est le numéro de ligne où vous voulez vous arrêter.

On va prendre ce fichier boucle.c comme fichier d'exemple :

#include <stdio.h>

void afficher_nombre(int nombre) {
    printf("%d\n", nombre);
}

int main() {
    printf("Je vais compter jusqu'à 100 !\n");
    for (int i = 1; i <= 100; i++) {
        afficher_nombre(i);
    }

    return 0;
}

Ce code fait bien ce qu'on veut, il n'y a pas de bug, mais je vais m'en servir pour montrer comment on peut utiliser GDB.

On peut mettre le programme en pause dès qu'on affiche un nombre avec avec break boucle.c:4.

Une fois qu'on est en pause, il y a différentes choses qui vont pouvoir nous aider à comprendre ce qui se passe. Déjà, GDB affiche ce genre de message :

Breakpoint 1, afficher_nombre (nombre=1) at boucle.c:4
4	    printf("%d\n", nombre);

Sur la première ligne, on voit l'endroit où on s'est arrêtés :

Sur la deuxième ligne, on voit le numéro de ligne à gauche, et le contenu de la ligne où on s'est arrêtés. Ces informations donnent déjà beaucoup d'aide dans le cas d'une erreur de segmentation : on peut savoir exactement quelle ligne pose problème, et essayer de réfléchir à ce qui ne va pas à partir de là.

Mais on peut obtenir plus d'information que ça !

Avec bt on peut inspecter la « backtrace », c'est à dire la liste des fonctions qui ont amené celle-ci à être appelée. Dans notre exemple, on voit :

#0  afficher_nombre (nombre=1) at boucle.c:4
#1  0x000000000040056c in main () at boucle.c:10

Chaque ligne corrspond à une fonction, la numéro 0, en haut est celle où on est actuellement. Celle en dessous est celle qui l'a appelée (le 0x000000000040056c peut être ignoré dans la plupart des cas, c'est le numéro de l'instruction qui a appelé afficher_nombre). Ici, on a que deux fonctions, mais on pourrait en avoir bien plus en dessous, si main avait été appelée par d'autres fonctions avant.

Par défaut, on est dans le contexte de la fonction #0, mais on peut se « déplacer » dans un autre contexte avec la commande frame NUMERO. GDB montre alors l'endroit où cette fonction était arrếtée (en général, c'est là où on appelait la fonction au dessus dans la liste).

Pour voir les valeurs des variables locales d'une fonction, on peut utiliser info locals. Il y a aussi info args pour afficher les valeurs des arguments de la fonction. Voici un exemple de commandes qui affichent les variables de afficher_nombre puis de main au moment où on est en pause :

(gdb) info args                                   # On affiche les arguments de afficher_nombre, il n'y a pas de variables locales
nombre = 1
(gdb) frame 1                                     # On se déplace dans le contexte de « main »
#1  0x000000000040056c in main () at boucle.c:10
10	        afficher_nombre(i);
(gdb) info locals                                 # On affiche les variables locales de main
i = 1

On peut aussi exécuter une instruction en C et afficher son résulat avec print EXPRESSION. Par exemple, si on est toujours arrêtés dans la fonction main, on peut faire :

print i + 2
$1 = 3

Le $1 est le résulat de notre calcul (i + 2 = 1 + 2 = 3).

On peut aussi demander à GDB d'appeler une fonction dès qu'on sort de la pause avec call EXPRESSION.

Enfin, une fois qu'on a vu ce qu'on voulait voir, on peut reprendre l'exécution avec continue ou la version abbrégée c. Le programme va tourner jusqu'à re-tomber sur un point d'arrêt.

Il existe d'autres commandes utiles dans GDB, vous pouvez les explorer avec help si vous êtes curieux⋅se.