#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; }
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.
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.
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.
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 :
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 !
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).
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 :
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!
Avec un éditeur, 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;
}
Cliquez sur le bouton Compile, et changez la commande de compilation en :
c++ -g divisions.cc -o divisions(si vous utilisez un scons pour compiler, ajoutez l'option -g à CCFLAGS (déjà mis dans les fichiers fournis pour le projet) )
La compilation s'exécute de la même façon que précédemment.
Lancez
alors le programme dans un terminal, par la commande divisions
(dans le répertoire où il se trouve). Le programme s'arrête avant
la fin, et vous obtenez un message d'erreur :
Floating exception (core dumped)
Le dévermineur va vous permettre de localiser l'erreur dans le programme et d'en déterminer la cause.
(Remarque : le signe '&' après une commande signifie que vous lancez la commande « en tâche de fond », c'est-à-dire libérée du terminal dans lequel vous l'avez lancée. Elle ne bloque donc plus votre interpréteur de commande (essayez sans le '&' et vous verrez que votre terminal est bloqué jusqu'à ce que ddd soit terminé))
Le dévermineur apparaît. Fermez la boîtes de dialogue 'Tip of the day'.
Sélectionnez dans le menu : View -> Data Window, puis View -> Command tool.
La fenêtre du dévermineur est alors composée de trois parties : en haut, une zone permettant l'affichage des variables du programme, au milieu une zone où s'affichera le code source du programme, en bas une zone de dialogue avec le dévermineur (gdb).
Vous devez voir de plus une mini-fenêtre, que vous pouvez déplacer indépendamment de la grande, et qui contient une série de boutons de commande.
Vous pouvez lancer le programme en cliquant sur Run dans la mini-fenêtre de commandes. Vous obtenez un message du genre :
Program received signal SIGFPE, Arithmetic exception.
0xef7770cc in main() at division.cc:14
...
Une flèche est apparue devant la ligne concernée dans la zone du milieu du dévermineur. C'est donc là que c'est produit l'erreur.
Si ce n'est pas le cas (configuration de ddd),
sélectionnez Status ->
Backtrace... dans les menus.
Une boîte de dialogue s'affiche, contenant la liste des appels de
fonction en cours. Repérez-y la ligne contenant le nom
du fichier que vous déverminez (divisions.cc). Elle doit
ressembler à :
#1 0x176d8 in main() at divisions.cc:14Elle indique que le programme s'est interrompu dans la fonction main du programme divisions.cc, à la ligne 14. Cliquez dessus : la flèche apparaît ! Fermez la boîte de dialogue.
Il existe divers moyens d'afficher la valeur des variables :
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. Pour suivre l'évolution des variables pendant l'exécution, commencez par les faire afficher avec la deuxième méthode, si ce n'est pas déjà fait.
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 avec le bouton droit, et sélectionnez Set breakpoint dans le menu qui apparaît. Un point d'arrêt apparaît sur la ligne sélectionnée, symbolisé par un panneau STOP.
Lancez le programme par le bouton Run. 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 :
Lorsque des variables sont affichées (par la commande Display()), leur valeur est mise à jour à chaque pas de l'exécution.
Exécutez le programme pas-à-pas en cliquant sur next, et observez l'évolution des valeurs des variables. À 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 !
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 durant le projet!)
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 au semestre prochain). 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 de ddd.
Commencez par lancer ddd : ddd test.
Initialement, ddd s'ouvre sur la fonction main. Pour visualiser un autre fichier source, sélectionnez le menu File > Open Source.... Dans la fenêtre qui s'ouvre alors, il se peut qu'il y ait beaucoup de fichiers listés. Pour n'en afficher que certains, vous pouvez remplir le champ Filter. Par exemple, pour ne lister que les fichiers dont le nom commence par Foo, entrez-y Foo*, où * sert de joker. Ensuite il suffit de sélectionner le fichier désiré dans la liste et d'appuyer Open.
Si vous vous intéressez à une fonction en particulier, vous pouvez y accéder directement en introduisant le nom de cette fonction directement dans le champs en dessous des menus puis appuyez sur entrer.
Essayez d'accèder à la fonction foo ou à la méthode Toto::doSomething.
Le menu Source de ddd offre la possibilité de numéroter les lignes du fichier source.
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.
A présent, lancez le programme test depuis ddd avec Run. Le programme devrait alors "planter lamentablement".
Pour visualiser la backtrace au moment du crash, entrez backtrace dans la partie inférieure de ddd.
Vous pouvez aussi le faire avec Status > Backtrace.
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'autres informations sont disponibles comme les arguments utilisés pour appeler une certaine fonction (foo (crash=true)) ou encore l'adresse de this pour les méthodes (Toto::doSomething (this=0x0)).
D'après la backtrace, le crash a lieu lors de l'appel de l'opérateur << fourni par la STL. Ce qui est très peu probable. Il faut garder en tête que le crash peut être dû à une erreur en amont dans le code. Pour remonter la trace d'exécution ("backtrace"), utilisez la fonction Up de ddd. Vous vous retrouvez à présent dans la méthode Toto::doSomething. Vous pouvez utiliser Down et Up pour parcourir la "backtrace".
ddd propose aussi une fenêtre pour sauter directement à une certaine fonction de la backtrace sans être obligé d'utiliser 100 fois Up ou Down.
Un dernier mot sur l'erreur de segmentation. L'erreur est dans Toto::doSomething mais pourtant le code semble valide. Le seul problème est qu'en fait this vaut nullptr et du coup l'accès à l'attribut str est indéterminé (ce qui provoque le crash). Dans la fonction failure on remarque que le pointeur ptr pointe sur le néant. ptr->doSomething(); aura pour cette raison un comportement indéterminé (il se peut que le programme ne plante pas). C'est une erreur assez fréquente!