5. Déboguer

5.1. Le débogueur

Le débogueur qui est fourni avec FreeBSD s'appelle gdb (GNU débogueur). Vous le lancez en tapant:

% gdb nom_du_programme
bien que la plupart des gens préfèrent l'exécuter sous Emacs. Ce qui se fait avec:
M-x gdb RET progname RET


Se servir d'un débogueur vous permet d'exécuter le programme sous contrôle. Vous pouvez typiquement l'exécuter pas à pas, inspecter les valeurs des variables, les modifier, dire au débogueur d'exécuter le programme jusqu'à un certain endroit et de s'y arrêter, et ainsi de suite. Vous pouvez même le rattacher à un programme qui est déjà en cours d'exécution, ou charger une image mémoire - “core”. Il est même possible de déboguer le noyau, bien que cela soit un peu plus compliqué que dans le cas des programmes utilisateurs, dont nous parlerons dans cette section.

gdb dispose d'une assez bonne aide en ligne, ainsi que d'un jeu de pages “info”, cette section se concentrera donc sur quelques commandes de base.

Enfin, si le mode de fonctionnement en ligne de commande vous rebute, il existe une interface graphique appelée xxgdb au catalogue des logiciels portés.

Cette section est destinée à servir d'introduction à l'utilisation du débogueur et ne couvre pas les questions spécialisées comme le débogage du noyau.

5.2. Exécuter un programme sous le débogueur

Il faudra que vous ayez compilé le programme avec l'option -g pour tirer le meilleur parti de gdb. Cela fonctionnera sans cela, mais vous ne verrez que le nom de la fonction dans laquelle vous êtes, au lieu du code source. Si vous avez un message du genre:

… (no debugging symbols found) …
au démarrage de gdb, vous saurez que le programme n'a pas été compilé avec l'option -g.

A l'invite de gdb, tapez break main. Cela dira au débogueur d'exécuter le code préliminaire de mise en oeuvre dans le programme et de s'arrêter au début de votre programme. Tapez maintenant run pour lancer le programme - il commencera au début du code préliminaire et sera interrompu par le débogueur sur l'appel de main(). (Si vous vous étiez jamais demandé d'où la fonction main() était appelée, vous le savez maintenant!).

Vous pouvez maintenant exécuter le programme une ligne à la fois, en appuyant sur n. Si vous arrivez sur un appel de fonction, vous pouvez passer dans la fonction en appuyant sur s. Une fois dans la fonction, vous pouvez terminer son exécution et en sortir en tapant f. Vous pouvez aussi utiliser up et down pour jeter un coup d'oeil au code appelant.

Voici un exemple simple de la manière de diagnostiquer une erreur dans un programme avec gdb. Voici notre programme (intentionnellement faux):


#include <stdio.h>

int bazz(int un_entier);

main() {
      int i;

      printf("C'est mon programme\n");
      bazz(i);
      return 0;
}

int bazz(int un_entier) {
      printf("Vous m'avez donné %d\n", un_entier);
      return un_entier;
}
       


Ce programme affecte à i la valeur 5 et la passe à la fonction bazz() qui affiche la valeur que nous lui donnons en paramètre.

Quand nous compilons et exécutons le programme, nous obtenons:

% cc -g -o temp temp.c
% ./temp
C'est mon programme
Vous m'avez donné 4231


Ce n'est pas ce à quoi nous nous attendions! C'est le moment d'aller voir ce qui ce passe!

% gdb temp
GDB is free software and you are welcome to 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.
GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc.
(gdb) break main				Exécuter le code d'initialisation
Breakpoint 1 at 0x160f: file temp.c, line 9.	gdb met un pont d'arrêt à l'appel de main()
(gdb) run					Exécuter jusqu'à main()
Starting program: /home/james/tmp/temp		Le programme démarre

Breakpoint 1, main () at temp.c:9		gdb s'arrête à main()
(gdb) n						Aller à la ligne suivante
C'est mon programme				Le programme imprime
(gdb) s						Aller dans bazz()
bazz (un_entier=4231) at temp.c:17		gdb affiche la position dans la pile d'appel
(gdb)


Une minute! Comment un_entier peut-il valoir 4231? Ne l'avons-nous pas initialisé à 5 dans main()? Revenons à main() et jetons un oeil.

(gdb) up					Remonter d'un cran dans la pile d'appel
#1  0x1625 in main () at temp.c:11		gdb affiche la position dans la pile d'appel
(gdb) p i					Afficher la valeur de i
$1 = 4231					gdb affiche 4231
Et oui! Si nous regardons le code, nous avons oublié d'initialiser i. Nous voulions mettre:

…
main() {
      int i;

      i = 5;
      printf("C'est mon programme\n");
…
     
mais nous avons oublié la ligne i=5;. Comme nous n'avons pas initialisé i, il prend la valeur qui se trouve à cet endroit de la mémoire quand le programme s'exécute, dans notre cas, il s'est trouvé que c'était 4231.

Note : gdb affiche l'endroit où nous nous trouvons dans la pile d'appel, chaque fois que nous entrons ou sortons d'une fonction, même si nous utilisons up et down pour nous déplacer dans la pile. Cela nous donne le nom de la fonction et les valeurs de ses paramètres, ce qui nous aide à repérer où nous sommes et ce qu'il se passe. (La pile d'appel est une zone de mémoire où le programme enregistre les informations sur les paramètres passés aux fonctions et où aller quand il ressort d'un fonction appelée.)

5.3. Examiner un fichier “core

Un fichier “core” est essentiellement un fichier qui contient l'état complet du programme au moment où il s'est “planté”. Au “bon vieux temps”, les programmeurs devaient imprimer le contenu en hexadécimal des fichiers “core” et transpirer sur des manuels de code machine, mais la vie est aujourd'hui un peu plus facile. Au passage, sous FreeBSD et les autres systèmes 4.4BSD, un fichier “core” s'appelle nom_du_programme.core, et non core tout court, de façon à ce que l'on sache à quel programme il correspond.

Pour examiner un fichier “core”, lancez gdb comme d'habitude. Au lieu de taper break ou run, tapez:

(gdb) core nom_du_programme.core
Si vous n'êtes pas dans le même répertoire que le fichier “core”, vous devrez d'abord faire dir /ou/se/trouve/le/fichier/core.

Vous devriez voir quelque chose comme:

% gdb a.out
GDB is free software and you are welcome to 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.
GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc.
(gdb) core a.out.core
Core was generated by `a.out'.
Program terminated with signal 11, Segmentation fault.
Cannot access memory at address 0x7020796d.
#0  0x164a in bazz (un_entier=0x5) at temp.c:17
(gdb)


Dans ce cas, le programme s'appelait a.out, le fichier “core” s'appelle donc a.out.core. Nous constatons que le programme s'est terminé en erreur à cause d'une tentative d'accès à une zone de mémoire qui n'était pas accessible dans une fonction appelé bazz.

Il est parfois utile de pouvoir savoir comment une fonction a été appelée, parce que le problème peut s'être produit bien au-dessus dans la pile d'appel dans un programme complexe. La commande bt dit à gdb d'imprimer en remontant la pile d'appel:

(gdb) bt
#0  0x164a in bazz (un_entier=0x5) at temp.c:17
#1  0xefbfd888 in end ()
#2  0x162c in main () at temp.c:11
(gdb)
La fonction end() est appelée quand un programme échoue; dans le cas présent, la fonction bazz() a été appelée par main().

5.4. Prendre le contrôle d'un programme en cours d'exécution

Une des possibilités les plus intéressantes de gdb est qu'il peut se rattacher à un programme en cours d'exécution. Il faut bien sûr que vous ayez les autorisations suffisantes pour le faire. Le cas d'un programme qui “fourche” - fork - est un problème classique, lorsque vous voulez suivre le déroulement du processus fils, alors que le débogueur ne vous permet que de tracer le processus père.

Vous lancez alors un autre gdb, utilisez ps pour connaître l'IDentifiant de processus du fils, puis faites:

(gdb) attach pid
sous gdb, et déboguez alors comme d'habitude.

“Tout cela est bien beau”, vous dites vous peut-être, “mais le temps que j'ai fait tout ça, le processus fils aura déjà fait un bon bout de chemin”. Rien à craindre, aimable lecteur, voici ce qu'il faut faire (emprunté aux pages “info” de gdb):

…
if ((pid = fork()) < 0)         /* _Toujours_ effectuer se contrôle */
      error();
else if (pid == 0) {            /* C'est le processus fils */
      int PauseMode = 1;

      while (PauseMode)
            sleep(10);          /* Attendre que quelqu'un se rattache à nous */
      …
} else {                        /* parent */
      …
Il n'y a plus qu'à se rattacher au fils, positionner PauseMode à 0, et attendre que l'appel de sleep() nous rende la main!

Ce document, ainsi que d'autres peut être téléchargé sur ftp.FreeBSD.org/pub/FreeBSD/doc/.

Pour toutes questions à propos de FreeBSD, lisez la documentation avant de contacter <questions@FreeBSD.org>.
Pour les questions sur cette documentation, contactez <doc@FreeBSD.org>.