Série 8 (Niveau 0):
Préparation au mini-projet : utilisation d'un dévermineur
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»:

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.
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 :

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 :

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» :

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 :

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 :

- commencer par placer la souris sur chacun des boutons du menu «Debugger», comme celui encadré en rouge ci-dessus, pour prendre connaissance du nom de chacun («Start debugger», «Step Over» etc.);
- pour exécuter une ligne à la fois, cliquez sur «Step Over» du menu du «Debugger» ;
- si l'instruction est un appel de fonction, il est possible d'exécuter pas-à-pas le corps de la fonction en utilisant le bouton «Step Into» (ce n'est pas utile dans cet exemple);
- pour continuer le programme jusqu'à la fin, sans s'arrêter à chaque ligne, cliquez sur le premier bouton du menu (qui s'appelait «Start Debugger», mais qui en cours de session de «debuging», s'appelle «Continue»). Pour redémarrer la session de debugging, vous pouvez cliquer sur l'«interrupteur» vert dans le menu «Debugger».
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 :

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 :
- stopper le programme au moyen du petit carré rouge dans le dévermineur;
- placer un point d'arrêt au début de la fonction failure;
- puis relancer l'exécution au moyen de la flèche verte.
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
- Attention, si vous travaillez sur vos propres machines sans la connexion à distance, le « debugger » de Geany ne fonctionne pas forcément bien sur tous les types de systèmes d'exploitation.
- Pour pouvoir utiliser ce « debugger », il faut avoir coché l'option Debugger sous Tools > Plugin manager. Si cette option n'apparaît pas, c'est que les «plugins Geany» ne sont pas installés sur votre ordinateur (voir : les indications pour linux ou l'installateur pour Windows).
- Sous Mac, selon la version du système d'exploitation utilisée, des problèmes de fonctionnement du « debugger » ont été reportés. Les utilisateurs Mac utilisent souvent plutôt le debugger intégré à Xcode (qui est d'un mode d'emploi très similaire).
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 :

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 :

- 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.
- cliquez sur le bouton Target ;
- sélectionnez l'exécutable de votre programme (dans le répertoire où est stocké le programme divisions.cc, mais sans l'extension .cc)
- 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 :

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

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 :

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 :

- pour exécuter une ligne à la fois, cliquez sur step over, (bouton encadré en bleu ci-dessus).
- si l'instruction est un appel de fonction, il est possible d'exécuter pas-à-pas le corps de la fonction en utilisant le bouton step into (ce n'est pas utile dans cet exemple);
- Pour continuer le programme jusqu'à la fin, sans s'arrêter à chaque ligne, cliquez sur la flèche verte.
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 :

À 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:

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:

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 :

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 :
- stopper le programme au moyen du petit carré rouge dans le dévermineur;
- placer un point d'arrêt au début de la fonction failure;
- puis relancer l'exécution au moyen de la flèche verte.

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