Un débogueur est un programme qui permet de suivre le déroulement d'un autre programme, de l'arrêter, d'ausculter l'état de la mémoire (valeur de variables par exemple), etc. ; ce qui est particulièrement utile pour rechercher des erreurs de programmation.

Nous expliquons ici les bases de l'utilisation d'un débogueur à l'aide du débogueur gdb à la ligne de commande, mais vous pouvez bien sûr utiliser des versions avec interface graphique, souvent intégrées dans les IDE ; les principes de base restent les mêmes ; en salles CO, vous avez par exemple ddd ou le module debuger intégré dans Geany pour lequel vous pouvez trouver un tutoriel là-bas (attention ! il s'agit d'un autre cours) ; pour d'autres GUI voir ce lien, parmi lesquelles nous vous recommandons gdbgui (site officiel ; site GitHub).

Vous pouvez aussi utiliser un autre débuger, comme par exemple lldb ; là aussi, les principes de base restent les mêmes. La correspondance entre les commandes gdb et lldb se trouve ici.

NOTE pour macOS : depuis OS X 10.9, Apple est passé à LLVM ; il n'y a donc plus gdb de base. Si vous êtes sur Mac, vous avez alors deux options :

  1. soit utiliser lldb ;

  2. soit installer gdb (via brew) et le signer ;
    OS X a un mécanisme de contrôle d'accès aux autres processus qui nécessite un binaire signé (ce qui est nécessaire pour un débuggeur) ;
    pour signer le binaire gdb après son installation, il faut suivre les instructions qu'on peut trouver sur Internet ; par exemple :

  • https://sourceware.org/gdb/wiki/PermissionsDarwin L'étape de création du certificat « gdb-entitlement.xml » est nécessaire pour les Mac avec système d'exploitation Big Sur.

Video Tutorial

Avant de lire la suite, nous vous proposons en guise d'introduction de regarder un tutoriel vidéo de 23 minutes, crée par Chris Bourke et disponible sur Youtube. Ce tutoriel explique comment trouver des erreurs dans du code en utilisant le débogueur gdb. La plupart des notions évoquées dans cette vidéo sont ensuite reprise pas à pas dans la suite de ce tutoriel.

Quelques remarques pour vous faciliter la comprehension de ce tutoriel vidéo :

  • À 1:08, Chris dit que le débogueur, dans son exemple, se lance avec la commande suivante : gdb a.out. Il faut remarquer que, pour vous, a.out devrait être remplacé par le nom de votre programme (executable).

  • À 1:14, il faut (temporairement) ignorer l'histoire d'arguments du programme. Vous verrez ca plus tard dans le cours.

  • À 2:34, il lance le programme avec la commande ./a.out. Ici aussi, utilisez le nom de votre programme (executable) à la place de a.out.

  • Jusqu'à 16:17, tout devrait être assez clair (sauf les arguments du programme en 1:14, comme dit ci-dessus). À partir de là, il utilise des notions de C non encore vues en cours. Vous pouvez donc arrêter ici cette vidéo et y revenir plus tard, ou continuer à la regarder pour voir comment utiliser gdb mais sans chercher à comprendre en profondeur les problèmes de C qui sont évoqués :

    • De 16:17 à 21:10, il montre comment corriger des erreurs liées aux arguments du programme. Ceux-ci seront abordés en semaine 10 du cours, mais les erreurs illustrées ici pourraient très bien avoir lieu sur d'autres variables ou tableaux.
    • À 21:10, il corrige une erreur liée à l'allocation (dynamique) de la mémoire, un concept qui sera vu en semaine 6 du cours.

Vous êtes maintenant prêts à lire et suivre les instructions et explications ci-dessous.

Ajouter les informations lors de la compilation

La première chose à faire pour pouvoir utiliser un débogueur est de demander au compilateur de mettre des informations supplémentaires dans le programme afin de permettre au débogueur de se repérer. Cela se fait en ajoutant l'option -g lors de la compilation. Par exemple :

gcc -g -o mon_programme mon_programme.c

Compilez de la sorte un des programmes fournis ; p.ex. :

gcc -g -std=c99 -o ex1 ex1.c

ou

gcc -g -std=c99 -o stats stats.c -lm

NOTE : nous utiliserons pour ce cours la norme C99 ; pour certains compilateurs, la compilation peut alors nécessiter l'ajout de l'option -std=c99 comme indiqué ci-dessus. Vous pouvez bien sûr aussi utiliser des normes plus récentes (p.ex. -std=c17).

Lancer le débogueur et l'exécution du programme

Ensuite, on peut exécuter le programme dans le débogueur. On lance pour cela le débogueur avec comme argument le programme à déboguer ; p.ex. :

gdb ./ex1

ou

gdb ./stats

On se retrouve dans le débogueur (c'est ici un interpréteur de commandes), dans lequel on ne voit pas grand chose pour le moment. Pour voir le code, tapez

layout src

Le code ne s'affiche pas encore car gdb n'a pas encore lancé notre programme. Lancez simplement son exécution avec la commande :

run

Le programme se déroule alors normalement (on peut déjà remarquer l'un ou l'autre bugs ;-). Tapez Ctrl-C pour l'arrêter quand vous en avez assez).

Tapez

quit

pour quitter le débogueur.

Points d'arrêt

Si ce n'est pas déjà fait, ouvrez le code stats.c dans un éditeur pour voir de quoi il s'agit.
Le but de ce programme est de calculer la moyenne et l'écart-type (non biaisé) de l'âge d'un ensemble de 1 à 1024 personnes.

Vous voyez au début du programme une variable nb_people qui est lue au clavier à la ligne 22. Utilisons le débogueur pour aller voir la valeur lue. Pour cela, relancez le débogueur sur notre programme :

gdb ./stats

puis

layout src

Mais cette fois, ajoutons un « point d'arrêt » (breakpoint) avant de lancer l'exécution. Cela se fait à l'aide de la commande break :

break 22

NOTE : pour en savoir plus sur cette commande, vous pouvez taper :

help break

Vous verrez alors que l'on peut non seulement indiquer des numéros de ligne, mais aussi des noms de fonctions (entre autres).
On peut par ailleurs mettre autant de point d'arrêt que l'on veut.

Une fois le point d'arrêt placé, lancer l'exécution :

run

Voir la valeur de variables

Cette fois le débogueur arrête l'exécution du programme à la ligne 22 et vous l'indique. Vous pouvez à ce stade donner des commandes au débogueur comme voir la valeur d'une variable, avancer d'un pas l'exécution du programme, continuer l'exécution ou ajouter un autre point d'arrêt.
Commençons par regarder la valeur de la variable nb_people :

print nb_people

vous affiche le résultat :

$1 = 0

($1 veut simplement dire que c'est la première expression que vous avez demandé qui est ici affichée).

NOTE : toutes les commande gdb peuvent être abrégées tant qu'elles ne sont pas ambiguës. Ici, on aurait donc simplement pu entrer :

p nb_people

A noter aussi qu'on a la complétion automatique avec la touche TAB. Essayez :

p nb_<TAB>

Mais il reste néanmoins fastidieux de toujours avoir à retaper des commandes print utiles. Il existe deux moyens d'éviter cela :

  1. gdb garde toutes les commandes en mémoire ; il suffit donc de naviguer dans l'historique avec les flèches (Haut et Bas) pour retrouver une commande déjà entrée ;
  2. la commande display affiche automatiquement l'expression demandée à chaque arrêt du débogueur (si tant est que l'expression fait sens à l'endroit de l'arrêt).

Essayons la commande display (on verra mieux son effet dans un instant) :

display nb_people

Exécution pas à pas

Essayons maintenant de continuer l'exécution.
Si vous ne savez plus où vous en êtes dans le programme, la commande :

where

vous l'indiquera (ici : dans la fonction main() à la ligne 22 du programme stats.c).

NOTE : where est en fait un alias pour backtrace ou bt, qui sont aussi souvent utilisés.

Pour avancer d'un pas, tapez :

next

Le débogueur exécute alors le scanf. C'est pour cela que vous avez le texte de la question qui apparaît.
Répondez-y.

Le débogueur vous indique alors s'être arrêté à la ligne 26 (vu qu'il n'y a pas de code aux lignes 23 à 25).
La commande next n'exécute en effet qu'une seule ligne du programme.
Si l'on avait voulu continuer l'exécution sans ne plus s'arrêter (en fait : continuer jusqu'au prochain point d'arrêt, mais comme nous n'en avons pas d'autre...), on aurait utilisé la commande (ATTENTION ! NE le faites PAS ici) :

cont

Vous pouvez également remarquer qu'en plus de la ligne 26, le débogueur vous a affiché la nouvelle valeur (celle saisie) de la variable nb_people. C'est le résultat de votre display précédent. Sans cette commande display, la nouvelle valeur n'aurait pas été affichée et il vous aurait fallu entrer un nouveau print pour la voir.

REMARQUES :

  1. next peut s'abréger n ;

  2. si l'on entre aucune commande, c'est simplement la commande précédente qui s'applique à nouveau ; cela est particulièrement pratique avec next : il suffit d'appuyer ensuite sur Enter plusieurs fois pour avancer pas à pas ;

  3. next peut être complété d'un nombre de répétitions :

     next 8
    

    fera par exemple 8 fois next ;
    next tout seul est donc la même chose de next 1 ;

next et step

Une confusion fréquente lors de la prise en main de débogueur est celle entre next et step :

  • next passe à l'expression suivante en restant au même niveau ; sans rentrer dans les sous-routines (= appel de fonctions) ;
  • step passe à la prochaine expression à évaluer, où qu'elle soit ; même si celle-ci est dans une sous-routine (et même si ce n'est pas une sous-routine à nous).

Illustrons cela en ajoutant un point d'arrêt supplémentaire un peu plus loin :

break 42

et continuez l'exécution jusque là-bas avec un simple :

cont

(répondez normalement aux questions).

Arrivé à la ligne 42, tapez

next

pour continuer. Vous voyez que la ligne 42 est exécutée et que l'on passe à la ligne 43.

Reprenons l'exemple en relançant l'exécution depuis le début :

run
y

Le débogueur arrête à nouveau l'exécution à la ligne 22. Comme cela ne nous intéresse plus, supprimons ce point d'arrêt :

info br

nous montre qu'il s'agit du point d'arrêt numéro 1 ; que l'on supprime :

delete 1

Puis l'on continue l'exécution :

cont

jusqu'à la ligne 42.

Si l'on tape maintenant step au lieu de next, on passe à la ligne...
...28 ? [Note : cela ne fonctionne pas sur macOS sur cet exemple (printf), mais fonctionnera avec vos propres fonctions.]

__printf (format=0x40094d "\nMoyenne    : %g\n") at printf.c:28
28      printf.c: No such file or directory.

Oui, 28 ! Mais pas de notre programme ; la ligne 28 de printf.c qui est le fichier qui a été compilé (il y a bien longtemps) pour donner le code de printf dans la bibliothèque C !
Et auquel nous n'avons pas accès (il n'est certainement pas sur votre ordinateur).

Que s'est il passé ?

Avec le step, nous sommes passés à la prochaine instruction C, qui se trouve en fait être à l'intérieur de printf lui-même (il a bien fallu l'écrire !!).

Essayez encore quelques step (au moins 7). Vous voyez que l'on « s'enfonce » dans la bibliothèque C...
Un

where

après plus de 7 step est d'ailleurs intéressant :

#0  _IO_vfprintf_internal (s=0x7ffff7ad1740 <_IO_2_1_stdout_>, format=0x40094d "\nMoyenne    : %g\n", ap=ap@entry=0x7fffffffdcc8) at vfprintf.c:1278
#1  0x00007ffff7781209 in __printf (format=<optimized out>) at printf.c:33
#2  0x0000000000400839 in main () at stats.c:42

Nous sommes dans une fonction _IO_vfprintf_internal qui a elle-même été appelée par une fonction __printf que nous avons appelée depuis la ligne 42 de notre programme.
Ca commence à ressembler aux messages d'exceptions de Java ;-) !

Comme on est perdu, terminons l'exécution du programme (et ce tutoriel) avec un simple

cont

Annexe 1 : liste des commandes vues dans ce tutoriel

  • layout src

  • run ou r

  • help

  • break NUMERO_DE_LIGNE ou br NUMERO_DE_LIGNE

  • break NOM_DE_FONCTION ou br NOM_DE_FONCTION

  • delete

  • info br

  • where ou bt (ou backtrace)

  • print ou p

  • display

  • cont ou c

  • next ou n

  • step ou s

Annexe 2 : déboguer un programme multi-process

Plus tard dans le projet, vous utiliserez peut être des tests unitaires avec la bibliothèque check. Mais ces tests unitaires se lancent un nouveau sous-processus par test (fork()) et c'est donc plus difficile à suivre. Si vous souhaitez debogguer avec gdb ces programmes de tests-unitaires, voici quelques compléments :

  1. entrez ces options dans gdb :

     set follow-fork-mode child
     set detach-on-fork off
    
  2. suivez dans quel sous-processus vous êtes avec la commande :

     info infe
    
  3. changez de processus avec infe suivi d'un numéro (tel qu'indiqué par info infe) ; p.ex. :

     infe 1
    
  4. ne mettez pas de breakpoints sur le code des unit-test-* eux-mêmes (car ils sont écrit avec des macros en fait), mais sur du « vrai » code C, soit celui des fonctions-outils utilisées pour ces tests, soit carrément sur votre propre code à vous.

Exemple :

Supposons que ce soit dans le 5e test que vous ayez des problèmes. Ce sera donc le 5e sous-processus qui vous intéresse.

Commencez alors comme d'habitude par lancer le débogueur sur le programme de tests-unitaires :

gdb ./unit-test-machin

Ajoutez les options suggérées :

set follow-fork-mode child
set detach-on-fork off

Mettez le breakpoint à l'endroit qui vous intéresse, p.ex. ici sur une fonction fait_machin_truc() :

break fait_machin_truc

Et lancez l'exécution dans le débogueur :

run

gdb s'arrêtera au premier break (ou alors au premier crash ;-)).
On regarde où l'on se situe :

info infe

On est p.ex. dans le 2e processus, c.-à-d. dans le 1er test (car le processus 1, c'est le main() et les tests créent un sous-processus à chaque fois) ; ce n'est pas celui-ci qui nous intéresse, donc on continue :

cont

gdb nous dit alors, par exemple, que le 2e process (« Inferior 2 ») est fini, mais il s'y trouve encore (faites « info infe » pour voir). Il faut donc ramener gdb au process père :

infe 1

et on continue :

cont

Il nous arrête à nouveau au breakpoint. On regarde à nouveau où l'on est :

info infe

...Et on continue comme ça jusqu'au breakpoint qui nous intéresse. Là on peut faire des next, display, print etc. comme d'habitude.

On peut comme celà « se promener » de processus en processus (infe <numero>) et savoir où on est (info infe).
Avec un peu d'habitude on arrive à s'y retrouver ;-)