Université
de la Méditerranée Faculté des Sciences de Luminy |
Algorithmique
& programmation en langage C
Henri Garreta |
Dune certaine manière, lapprentissage dun langage de programmation est un exemple denseignement assisté par ordinateur : quiconque possède un document décrivant le langage, un compilateur pour traduire les programmes et un environnement dexécution pour les faire tourner peut sengager dans une auto-formation dans ce langage.
En réalité, lexercice est ardu et requiert une forte dose de courage. Il faut déjà beaucoup dhumilité et de discipline pour lire et comprendre les diagnostics du compilateur (croyez-moi, « parse error before printf » nest pas une insulte, mais une indication utile), mais cest après la compilation que commencent les vraies difficultés. Que faire quand lexécution du programme ne se passe pas comme prévu ? Quil se « plante » au milieu (erreur de segmentation, vous voyez ce que je veux dire...) ou quil aille jusquau bout mais en donnant des résultats erronés, il faut parfois beaucoup de temps pour trouver lerreur.
Un débogueur est un outil permettant dexécuter un programme en larrêtant à des endroits intéressants et en avançant alors instruction par instruction, tout en examinant le contenu des variables. Cela peut faire gagner énormément de temps dans la recherche des causes qui font quune variable nait pas la valeur quelle devrait avoir ou que lexécution ne passe pas par là où elle devrait.
Le débogueur expliqué ici est DDD, présent dans la plupart des distributions de Linux. DDD est une interface graphique servant de façade à GDB, le débogueur en ligne de commande du projet GNU.
La documentation officielle de DDD, très complète, se trouve ici. Un article sur DDD en [espagnol, traduit en] français est ici.
N.B. Si vous utilisez Dev-C++ sur Windows vous disposez également dun débogueur avec une interface graphique ; des indications à ce propos sont données dans la note Utiliser Dev-C++. Bien entendu, Visual C++, C++Builder et tous les autres environnement de développement sérieux comportent leurs propres débogueurs, en général très puissants.
Pour lessentiel, les notes qui suivent sont une traduction de la section A Sample DDD Session de la documentation officielle.
Supposons avoir écrit un programme, nommé sample (vous pouvez voir ici le source sample.c). En principe, ce programme doit trier les nombres qui sont ses arguments par ordre croissant, selon une méthode de tri appelée Shell Sort, et les imprimer, comme dans lexemple suivant
$ ./sample 8 7 5 4 1 3 1 3 4 5 7 8 $
Cependant, avec certains arguments cela ne marche pas bien :
$ ./sample 8000 7000 5000 1000 4000 1000 1913 4000 5000 7000 $
Même si la sortie est triée et contient le bon nombre de valeurs, certains arguments ont été perdus et remplacés par des valeurs bidon. Ici, 8000 a disparu, remplacé par 1913.
Utilisons DDD pour voir ce qui se passe. Dabord, nous devons compiler notre programme en vue de le déboguer, cest à dire en incluant loption « -g »1 :
$ gcc -g sample.c -o sample
Maintenant nous pouvons activer DDD sur lexécutable sample :
$ ddd sample
Après quelques secondes, DDD arrive. La Source Window contient le texte source du programme débogué ; vous pouvez utiliser la Scroll Bar pour vous promener dans ce fichier.
La Debugger Console (en bas) contient de linformation sur la version de DDD, ainsi que l« invite de commandes » de GDB2
GNU DDD Version 3.2, by Dorothea Lütkehaus and Andreas Zeller. Copyright © 1999 Technische Universität Braunschweig, Germany. Copyright © 1999 Universität Passau, Germany. Reading symbols from sample...done. (gdb)
La première chose à faire cest de placer un point darrêt (ou breakpoint), qui fera sarrêter sample à lendroit qui nous intéresse. Cliquez dans lespace blanc à gauche de linitialisation de a. Le Argument field (): contient maintenant la position (sample.c:31). Maintenant, cliquez sur Break pour créer un point darrêt à lemplacement dans (). Vous voyez un petit signal stop rouge apparaître dans la ligne 31.
La prochaine chose à faire est dexécuter effectivement le programme, afin dexaminer son comportement. Faites la commande Program => Run (i.e. la commande Run du menu Program) ; la boîte de dialogue Run Program apparaît.
Dans le champ Run with Arguments vous pouvez saisir les arguments pour le programme sample. Saisissez les arguments qui produisent un comportement erroné -- cest à dire 8000 7000 5000 1000 4000. Cliquez sur Run pour démarrez lexécution avec les arguments que vous venez de saisir.
Maintenant GDB fait démarrer sample. Lexécution stoppe au bout dun instant, lorsque le point darrêt est atteint. Cela est signalé dans la Debugger Console.
(gdb) break sample.c:31 Breakpoint 1 at 0x8048666: file sample.c, line 31. (gdb) run 8000 7000 5000 1000 4000 Starting program: sample 8000 7000 5000 1000 4000 Breakpoint 1, main (argc=6, argv=0xbffff918) at sample.c:31 (gdb)
La ligne couramment exécutée (en réalité, la ligne qui va être exécutée) est indiquée par une flèche verte
=> a = (int *)malloc((argc - 1) * sizeof(int));
Vous pouvez maintenant examiner les valeurs des variables. Pour examiner une variable simple, il suffit de placer le pointeur de la souris sur son nom et le laisser dessus. Au bout dune seconde, une petite fenêtre surgit montrant avec la valeur de la variable. Essayez avec argv pour voir sa valeur (6). La variable locale a nest pas initialisée ; vous verrez probablement 0x03 ou une autre valeur de pointeur invalide.
Pour exécuter la ligne courante, cliquez sur le bouton Next de la Commande Tool. La flèche avance sur la ligne suivante. Maintenant, pointez de nouveau sur a pour voir que la valeur a changé et que cette variable a bien été initialisée.
Pour examiner une valeur individuelle du tableau a, par exemple la première, saisissez a[0] dans le Argument Field (vous pouvez effacer dabord de champ en cliquant sur ():) et alors cliquez sur le bouton Print. Cela affiche la valeur courante de () dans la Debugger Console. Dans notre cas, on obtient
(gdb) print a[0] $1 = 0 (gdb)
ou quelque autre valeur (notez que a a été alloué, mais son contenu na pas été initialisé).
Pour voir tous les membres de a en même temps, vous devez utiliser un opérateur spécial de GDB. Puisque a a été alloué dynamiquement, GDB ne connaît pas sa taille ; vous devez explicitement utiliser lopérateur @ pour indiquer une « tranche de tableau ». Saisissez a[0]@(argc - 1) dans le Argument Field et cliquez sur le bouton Print. Vous obtenez les premiers argc - 1 éléments de a, soit
(gdb) print a[0]@(argc - 1) $2 = {0, 0, 0, 0, 0} (gdb)
Au lieu dutiliser Print à chaque arrêt pour voir la valeur courante de a, vous pouvez aussi afficher a, qui deviendra donc constamment visible, avec sa valeur changeante. Ayant toujours a[0]@(argc - 1) visible dans le Argument Field, cliquez sur Display. Le contenu de a est maintenant montré dans une nouvelle fenêtre, la Data Window. Cliquez sur Rotate pour faire tourner horizontalement le tableau.
Arrive maintenant laffectation des membres de a :
=> for (i = 0; i < argc - 1; i++) a[i] = atoi(argv[i + 1]);
Cliquez sur Next et encore sur Next pour voir comment les membres individuels de a sont affectés les uns après les autres. Les membres changés sont mis en relief.
Pour relancer lexécution (rapide) de la boucle vous pouvez utiliser le bouton Until. Son effet est que GDB exécute le programme jusquà une ligne supérieure à la ligne courante. Cliquez sur Until jusquà vous trouver sur lappel de shell_sort dans
=> shell_sort(a, argc);
A ce point, le contenu de a est 8000 7000 5000 1000 4000. Cliquez sur Next pour avancer par dessus5 lappel de shell_sort. DDD sarrête sur
=> for (i = 0; i < argc - 1; i++) printf("%d ", a[i]);
et vous voyez que lorsque shell_sort a terminé, les valeurs de a sont 1000 1913 4000 5000 7000 -- cest-à-dire que pour une raison ou une autre, shell_sort a fichu la pagaille dans le contenu de a.
Pour découvrir ce qui est arrivé, exécutez encore une fois le programme. Cette fois vous allez sauter linitialisation de a et vous rendre directement à lappel de shell_sort. Effacez lancien point darrêt en le sélectionnant et en cliquant sur le bouton Clear. Ensuite créez un nouveau point darrêt dans la ligne 35 juste avant lappel de shell_sort. Pour exécuter le programme de nouveau, sélectionnez Program => Run Again.
DDD sarrête avant lappel de shell_sort :
=> shell_sort(a, argc);
Cette fois vous voulez examiner de près ce que shell_sort fait. Cliquez sur Step4 pour avancer à linterieur de lappel de shell_sort. Cela laisse votre programme dans la première ligne exécutable de la fonction, soit
=> int h = 1;
tandis que la Debugger Console vous informe sur la fonction dans laquelle vous venez de rentrer
(gdb) step shell_sort (a=0x8049878, size=6) at sample.c:9 (gdb)
Cet affichage qui montre la fonction dans laquelle sample est maintenant arrêté est appelé un « stack frame display » (quon pourrait traduire par affichage de la structure locale). Cela montre un résumé de la pile dexécution. A tout moment vous pouvez utiliser Status => Backtrace pour voir où en est la pile dexécution (cest-à-dire quelles fonctions ont été appelées, avec quels arguments, etc.) ; cliquer sur une ligne, ou cliquer les boutons Up et Down , vous permet de vous promener dans la pile.
Examinons si les arguments de shell_sort sont corrects. Revenez - si besoin - dans la stack frame inférieure, saisissez a[0]@size dans le Argument Field et cliquez sur le bouton Print :
(gdb) print a[0] @ size $4 = {8000, 7000, 5000, 1000, 4000, 1913} (gdb)
Surprise! Doù vient cette valeur 1913 supplémentaire ? La réponse est simple : la taille du tableau passée à shell_sort dans le paramètre size est trop grande de une unité. Le nombre 1913 est une valeur imprévisible qui se trouvait dans la mémoire à la suite de a. Mais cette valeur va être triée avec les autres.
Pour voir si tel est effectivement le problème, vous pouvez affecter à size la valeur correcte. Choisissez size dans le code source et cliquez sur Set. Une boîte de dialogue apparaît dans laquelle vous pouvez éditer (i.e. changer) la valeur de la variable
Donnez à size la valeur 5 et cliquez sur OK. Ensuite, cliquez sur Finish pour relancer lexécution de la fonction shell_sort.
(gdb) set variable size = 5 (gdb) finish Run till exit from #0 shell_sort (a=0x8049878, size=5) at sample.c:9 0x80486ed in main (argc=6, argv=0xbffff918) at sample.c:35 (gdb)
Succès! Laffichage de a contient maintenant les valeurs correctes 1000 4000 5000 7000 8000.
Vous pouvez vérifier que ces valeurs seront effectivement affichées sur la sortie standard dans lexécution restante du programme. Cliquez sur Cont pour continuer lexécution.
(gdb) cont 1000 4000 5000 7000 8000 Program exited normally. (gdb)
Le message « Program exited normally. » provient de GDB ; il indique que le programme sample a fini de sexécuter.
Ayant trouvé la cause du problème vous pouvez maintenant corriger le code source. Cliquez sur Edit pour éditer sample.c et remplacez la ligne
shell_sort(a, argc);
par lappel correct
shell_sort(a, argc - 1);
Vous pouvez recompiler sample
$ gcc -g -o sample sample.c
et vérifier (via Program => Run Again) que sample fonctionne maintenant correctement
(gdb) run `sample has changed; re-reading symbols. Reading in symbols...done. Starting program: sample 8000 7000 5000 1000 4000 1000 4000 5000 7000 8000 Program exited normally. (gdb)
Cest tout. Le programme est correct maintenant. Vous pouvez terminer la session DDD par la commande Program => Exit ou bien les touches Ctrl+Q.
1. Loption « -g » est nécessaire pour produire un exécutable qui contienne les noms des variables et des renvois vers les lignes du fichier source.
2. Si vous ne voyez pas une invite (gdb) ici rappelez DDD, par la commande --gdb.
3. Les valeurs des pointeurs sont montrées en hexadécimal (des nombres commençant par 0x).
4. Cest sur lappel dune fonction que les boutons Step et Next différent : Step permet dexaminer lintérieur de la fonction, sarrêtant sur sa première ligne, tandis que Next considère un appel de fonction comme une instruction atomique dont il nest pas utile dexaminer lintérieur.