Choisissez votre style : colorisé, impression

Série 8  (Niveau 0):
Préparation au mini-projet : utilisation d'un dévermineur


Le but de cet exercice est de vous montrer comment utiliser un dévermineur (« debbuger »). Les dévermineurs sont des outils vous permettant de traquer les problèmes d'exécution dans un programme. L'utilisation d'un dévermineur est vivement recommandée dans le cadre du projet.


Debugger de QtCreator


Debugger de Geany


Déverminage avec QTCreator

Pour réaliser cet exercice, commencez par créer un projet QtCreator nommé divisions (dans le répertoire de la série en cours) comme indiqué dans le tutoriel sur la création de projet avec un seul fichier. Copiez dans le programme principal le programme suivant :
#include <iostream>
using namespace std;

int main()
{
  int a, b;
  a = 1024;
  b = a*a*a-1;
  a = 2*b;
  b = a+1;
  a = b+1;
  b = 4*b;
  a = 2*a;
  b = b/a;
  cout << b << endl;
  return 0;
}

1. Compiler pour le déverminage

En C++, il est nécessaire de compiler avec l'option -g afin de pouvoir exécuter le code sous «debugger». Dans QtCreator, afin d'activer cette option, assurez vous simplement que le projet est configuré en mode «Debug»:

Option debug dans QtCreator

Lancez la compilation au moyen du petit marteau en bas à gauche. La fenêtre «Issues» ne devrait révéler aucun problème. Si vous lancez l'exécution au moyen de la petite flèche verte en bas à gauche, la fenêtre «Application output» revèle cependant un crash du programme :

   divisions...
   <chemin_vers_votre_projet>/divisions crashed. 
    

Ceci se produit pour la plupart des implémentations du compilateur mais il peut arriver que ce ne soit pas le cas (et une valeur arbitraire résulte du calcul). Dans ce cas vous êtes invités à pratiquer le reste de l'exercice en plaçant les points d'arrêt et en observant tout de même ce que le programme fait.

Le dévermineur (« Debugger ») intégré à QtCreator va vous permettre de localiser l'erreur dans le programme et d'en déterminer la cause.

2. Lancer le dévermineur

Pour lancer l'exécution du programme au moyen du dévermineur, cliquez sur la flèche verte avec un petit «insecte» dessus :

lancer un programme sous debugger dans QtCreator

Vous verrez alors s'afficher une fenêtre d'alerte indiquant que le programme a «planté» ainsi qu'une petite flèche jaune indiquant l'instruction fautive qui a déclenché cette situation :

crash sous debugger dans QtCreator

Cliquez sur le bouton «OK» pour fermer le fenêtre d'alerte.

2.Afficher la valeur des variables

Un premier pas vers l'identification des causes de l'erreur consiste à examiner la valeur des variables impliquées dans la ligne fautive.

Faites le pour les variables a et b, simplement en plaçant votre curseur dessus (à noter que la valeur des variables apparaît aussi sur le panneau latéral droit qui s'ouvre lors de l'exécution en mode «Debug»).

Vous devez pouvoir ainsi observer les valeurs a=0 et b=-4. Ce sont les valeurs des variables au moment où l'erreur a été détectée. La cause de l'erreur devient évidente : la division par a=0.

Dans la suite, vous allez exécuter le programme pas-à-pas, pour comprendre à quel moment les résultats des calculs deviennent aberrants.

3. Exécuter le programme pas-à-pas

Arrêtez le programme en cliquant sur le petit carré rouge du menu «Debugger» :

Arrêt de l'exécutiona sous debugger (QtCreator)

Pour exécuter le programme pas-à-pas, il faut commencer par mettre un point d'arrêt (breakpoint) à l'endroit où l'on veut commencer l'observation. Dans cet exemple, on va observer le déroulement du programme depuis le début, c'est-à-dire depuis la première ligne après «main() {». Cliquez sur cette ligne dans la marge où apparaissent les numéros de ligne avec le bouton droit de la souris. Un point d'arrêt apparaît sur la ligne sélectionnée, symbolisé par petit point rouge :

 installer un point d'arrêt (QtCreator)

Lancez alors le programme avec la flèche verte du menu «Debugger» (à côté du carré rouge que vous avez utilisé précédemment pour stopper le debugger). Le programme démarre son exécution et s'arrête à la première instruction suivant le point d'arrêt. La flèche dans la zone de programme indique la prochaine ligne qui doit être exécutée :

Arrêt au breakpoint (QtCreator)

Exécutez le programme pas-à-pas en cliquant sur «Step Over», et observez l'évolution des valeurs des variables.

À quel moment ces valeurs deviennent-elles aberrantes ?

Le but de cet exercice est de vous faire exécuter un programme pas-à-pas en suivant l'évolution des variables, et non de comprendre pourquoi le programme divisions.cc se comporte bizarrement.

Voici cependant, à titre documentaire, l'explication succincte de son comportement :

Le programme a un comportement anormal à partir de la ligne

a = b+1

En effet, à ce moment-là, la valeur de b est la plus grande valeur possible pour une variable de type int. En effet le type int n'est pas un vrai type entier au sens mathématique du terme. Les variables de ce type sont en fait bornées dans l'intervalle [-MAX_INT - 1, MAX_INT].

Pour l'ordinateur, si b=MAX_INT, alors b+1 = -MAX_INT - 1 !!!

Et si a=-MAX_INT - 1, alors 2*a = 0 !!!

Bref, dès que l'on dépasse les capacités de représentation, les résultats donnent n'importe quoi du point de vue de l'arithmétique !

Le tout est de le savoir  !

4. Programme avec plusieurs sources

Fermez le projet divisions dans QtCreator.

Pour cette sous-section, téléchargez l'exemple fourni et désarchivez-le dans un dossier de votre choix créé dans celui de la série en cours (depuis le terminal vous pouvez exécuter unzip test-debug-project.zip).

Il s'agit d'un programme constitué de plusieurs fichiers (le but étant de vous montrer comment l'outil de déverminage vous permet de naviguer entre plusieurs fichiers source. Ce que vous serez amenés à pratiquer intensivement durant le projet!).

Créez un projet QtCreator comme indiqué dans le tutoriel sur la création d'un projet compilable avec Cmake (il vous suffit d'ouvrir le fichier CmakeLists.txt du répertoire src dans l'archive fournie).

Commencez par ouvrir tous les fichiers impliqués dans QtCreator: main.cpp, foo.cpp, bar.cpp. Vous constaterez que le programme principal main.cpp inclut les fonctionnalités du fichier foo.cpp lequel utilise celles de bar.cpp (ceci se fait au moyen des directives d'inclusions et nous l'étudierons plus en détail la semaine prochaine). Sélectionnez la fenêtre contenant le programme principal (main.cpp) et lancez la compilation au moyen du petit marteau.

Lancez ensuite le programme sous «Debugger», comme vous l'avez fait pour le projet avec un seul fichier. Le programme s'arrête et une flèche indique que l'instruction de la ligne 7 du programme principal est fautive. Cette instruction en tant que telle ne comporte cependant rien d'anormal et implique l'appel à la fonction foo placée dans un autre fichier (foo.cpp). Continuez l'exécution au moyen de la flèche verte du menu «Debugger». Une fenêtre d'alerte indiquant un «crash» du programme s'ouvre. La flèche jaune indique que c'est la ligne 13 du fichier bar.cpp qui est fautive. En plaçant le curseur sur la variable str vous verrez une information indiquant que la valeur est impropre (c'est le 0x0 qui s'affiche dans la panneau latéral droit).

5. Pile des appels

Pour situer plus finement la source de l'erreur, il est nécessaire d'examiner l'enchaînement des appels de fonctions ayant abouti à l'erreur («Stack trace» ou «Backtrace»). Dans QtCreator cette pile des appels est visualisée dans le panneau encadré en rouge dans la figure ci-dessous :

Pile des appels (QtCreator)

Il s'agit de la liste des fonctions que le programme a exécuté jusqu'à un moment donné, par exemple un crash ou un breakpoint.

Les fonctions exécutées par le programme sont listées de la plus récente à la plus ancienne avec, pour chaque fonction, le nom du fichier source où elle est implémentée et la dernière ligne exécutée dans la fonction (par exemple main.cpp:7) (déroulez au besoin la fenêtre contenant la pile des appels pour voir ces informations).

Vous pouvez cliquer sur chacune des fonctions dans la backtrace et verrez à chaque fois la dernière instruction exécutée marquée par une petite flèche dans la marge.

D'après la backtrace, la toute dernière instruction provoquant le crash a lieu lors de l'appel de l'opérateur << fourni par la librairie standard  (ligne 13 de bar.cpp).

Il est très peu probable que ce soit l'opérateur << qui soit erroné! Il faut garder en tête que le crash peut être dû à une erreur en amont dans le code. Remontez alors d'un cran dans la pile des appels (il peut être parfois nécessaire de remonter plus haut). Vous vous retrouverez au niveau de la fonction failure. L'erreur saute en principe aux yeux (accès via un pointeur nul), mais supposons que ce soit moins évident. La chose à faire ici serait de :

L'examen du contenu des variables vous montrera alors le pointeur nul.

Dans la «vraie vie», il faudrait alors comprendre pourquoi ce pointeur a une telle valeur et apporter la correction nécessaire. Ce type d'erreur est assez fréquent!


Déverminage avec Geany

1. Compiler pour le déverminage

Dans Geany, ouvrez un fichier divisions.cc, et introduisez-y le programme suivant :

#include <iostream>
using namespace std;

int main()
{
  int a, b;
  a = 1024;
  b = a*a*a-1;
  a = 2*b;
  b = a+1;
  a = b+1;
  b = 4*b;
  a = 2*a;
  b = b/a;
  cout << b << endl;
  return 0;
}

Important : pour pouvoir utiliser le dévermineur associé à Geany, il faut régler les options de compilation pour le compiler avec l'option -g : Build > Set Build Commands :

Option -g dans Geany

Lancez la compilation de divisions.cc dans Geany (bouton F9). Tout devrait se passer comme d'habitude. Si vous lancez l'exécution (dans Geany ou dans un terminal), le programme s'arrête avant la fin, et vous obtenez un message d'erreur : Floating exception (core dumped)

Le dévermineur (« Debugger ») va vous permettre de localiser l'erreur dans le programme et d'en déterminer la cause.

2. Lancer le dévermineur

Pour lancer l'exécution du programme au moyen du dévermineur :

lancer un programme sous debugger dans  Geany

  1. cliquez sur le bouton Debug (en bas à gauche) dans Geany 
    Si ce bouton n'est pas visible, activez le debugger sous Tools > Plugin Manager puis cochez l'option Debugger.
  2. cliquez sur le bouton Target ;
  3. sélectionnez l'exécutable de votre programme (dans le répertoire où est stocké le programme divisions.cc, mais sans l'extension .cc
  4. puis cliquez sur la petite flèche verte en haut à droite de la fenêtre de «debugging».

Vous devriez voir s'afficher une fenêtre d'alerte indiquant que le programme s'est terminé avec une erreur. Lorsque vous fermez cette fenêtre vous pouvez voir que la ligne de code ayant provoqué l'erreur est désignée par une flèche dans Geany :

ligne fautive (debugger de Geany)

2.Afficher la valeur des variables

Un premier pas vers l'identification des causes de l'erreur consiste à examiner la valeur des variables impliquées dans la ligne fautive.

Faites le pour les variables a et b, simplement en plaçant votre curseur dessus

examen de la valeur des variable (debugger Geany)

L'information sur la valeur de la variable disparaît dès que vous déplacez le pointeur.

Vous devez pouvoir ainsi observer les valeurs a=0 et b=-4. Ce sont les valeurs des variables au moment où l'erreur a été détectée. La cause de l'erreur devient évidente : la division par a=0.

Dans la suite, vous allez exécuter le programme pas-à-pas, pour comprendre à quel moment les résultats des calculs deviennent aberrants.

3. Exécuter le programme pas-à-pas

Arrêtez le programme en cliquant sur le petit carré rouge en dessous de la flèche verte que vous avez utilisée pour lancer le programme dans le debugger.

Pour exécuter le programme pas-à-pas, il faut commencer par mettre un point d'arrêt (breakpoint) à l'endroit où l'on veut commencer l'observation. Dans cet exemple, on va observer le déroulement du programme depuis le début, c'est-à-dire depuis la première ligne après "main() {". Cliquez sur cette ligne dans la marge où apparaissent les numéro de ligne avec le bouton droit de la souris. Un point d'arrêt apparaît sur la ligne sélectionnée, symbolisé par petit losange rouge :

 installer un point d'arrêt (debugger Geany)

Lancez alors le programme avec la flèche verte. Il s'arrête à la première instruction suivant le point d'arrêt. La flèche dans la zone de programme indique la prochaine ligne qui doit être exécutée :

Arrêt au breakpoint

Exécutez le programme pas-à-pas en cliquant sur Step Over, et observez l'évolution des valeurs des variables.

Vous noterez que lorsque vous exécutez pas à pas vous pouvez aussi examiner le contenu des variables en sélectionnant l'onglet Autos :

Autos (Geany debugger)

À quel moment ces valeurs deviennent-elles aberrantes ?

NB : Le but de cet exercice est de vous faire exécuter un programme pas-à-pas en suivant l'évolution des variables, et non de comprendre pourquoi le programme divisions.cc se comporte bizarrement.

Voici cependant, à titre documentaire, l'explication succincte de son comportement :

Le programme a un comportement anormal à partir de la ligne

a = b+1

En effet, à ce moment-là, la valeur de b est la plus grande valeur possible pour une variable de type int. En effet le type int n'est pas un vrai type entier au sens mathématique du terme. Les variables de ce type sont en fait bornées dans l'intervalle [-MAX_INT - 1, MAX_INT].

Pour l'ordinateur, si b=MAX_INT, alors b+1 = -MAX_INT - 1 !!!

Et si a=-MAX_INT - 1, alors 2*a = 0 !!!

Bref, dès que l'on dépasse les capacités de représentation, les résultats donnent n'importe quoi du point de vue de l'arithmétique !

Le tout est de le savoir  !

4. Programme avec plusieurs sources

Fermez le fichier divisions.cc dans Geany.

Pour cette sous-section et la suivante, téléchargez l'exemple fourni et désarchivez-le dans le dossier de votre choix (depuis le terminal vous pouvez exécuter unzip dddTest).

Il s'agit d'un programme constitué de plusieurs fichiers (le but étant de vous montrer comment l'outil de déverminage vous permet de naviguer entre plusieurs fichiers source. Ce que vous serez amenés à pratiquer intensivement au semestre de printemps!)

Rendez-vous dans un terminal dans ce dossier et exécutez make afin de compiler le programme (ne vous préoccupez pas de cet aspect, nous reviendrons à la compilation séparée en temps voulu). Vous pouvez à présent lancer le programme avec ./test. Vous remarquerez que le programme ne fonctionne pas ("plante"). Nous allons voir pourquoi et en profiter pour explorer certaines fonctionnalités du dévermineur de Geany.

Commencez par ouvrir tous les fichiers impliqués dans Geany: main.cpp, foo.cpp, bar.cpp. Sélectionnez la fenrêtre contenant le programme principal (main.cpp).

La commande make a en fait produit dans le répertoire où se trouvent ces fichiers un exécutable nommé test qu'il faudra spécifier comme nouvelle cible du dévermineur via Debug > Target:

Geany debugger

Lancez alors le programme au moyen de la flèche verte, une petite fenêtre s'affiche indiquant qu'une erreur s'est produite. Lorsque l'on ferme cette fenêtre, une flèche indique que l'instruction de la ligne 7 du programme principal est fautive. Cette instruction en tant que telle ne comporte cependant rien d'anormal et implique l'appel à la fonction foo placée dans un autre fichier.

Pour situer plus finement la source de l'erreur, il est nécessaire d'examiner l'enchaînement des appels de fonctions ayant abouti à l'erreur. Il faut dans ce cas utiliser la pile des appels comme expliqué ci-dessous.

5. Backtrace

La backtrace d'un programme est la liste des fonctions qu'il a exécutées jusqu'à un moment donné, par exemple un crash ou un breakpoint.

Pour visualiser la backtrace au moment du crash que nous venons de provoquer, utilisez le bouton Call Stack:

Call stack (Geany debugger)

Les fonctions exécutées par le programme sont listées de la plus récente à la plus ancienne avec, pour chaque fonction, le nom du fichier source où elle est implémentée et la dernière ligne exécutée dans la fonction (par exemple main.cpp:7) (déroulez sur la droite la fenêtre contenant la pile des appels pour voir ces informations).

Vous pouvez cliquer sur chacune des fonctions dans la backtrace et verrez à chaque fois la dernière instruction exécutée marquée par une petite flèche dans la marge.

D'après la backtrace, la toute dernière instruction provoquant le crash a lieu lors de l'appel de l'opérateur << fourni par la librairie standard :

Call stack (Geany debugger)

Il est très peu probable que ce soit l'opérateur << qui soit erroné! Il faut garder en tête que le crash peut être dû à une erreur en amont dans le code. Remontez alors d'un cran dans la pile des appels (il peut être parfois nécessaire de remonter plus haut). Vous vous retrouverez au niveau de la fonction failure. L'erreur saute en principe aux yeux (accès via un pointeur nul), mais supposons que ce soit moins évident. La chose à faire ici serait de :

L'examen du contenu des variables vous montrera alors le pointeur nul:

null pointer segmentation fault (Geany debugger)

Dans la «vraie vie», il faudrait alors comprendre pourquoi ce pointeur a une telle valeur et apporter la correction nécessaire. Ce type d'erreur est assez fréquent!


Retour à la série

Dernière mise à jour : $Date: 2024/10/17 12:15 $