Université de la Méditerranée
Faculté des Sciences de Luminy

Algorithmique & programmation en langage C
Henri Garreta

D’une certaine manière, l’apprentissage d’un langage de programmation est un exemple d’enseignement assisté par ordinateur : quiconque possède un document décrivant le langage, un compilateur pour traduire les programmes et un environnement d’exécution pour les faire tourner peut s’engager dans une auto-formation dans ce langage.

En réalité, l’exercice est ardu et requiert une forte dose de courage. Il faut déjà beaucoup d’humilité et de discipline pour lire et comprendre les diagnostics du compilateur (croyez-moi, « parse error before printf » n’est pas une insulte, mais une indication utile), mais c’est après la compilation que commencent les vraies difficultés. Que faire quand l’exécution du programme ne se passe pas comme prévu ? Qu’il se « plante » au milieu (erreur de segmentation, vous voyez ce que je veux dire...) ou qu’il aille jusqu’au bout mais en donnant des résultats erronés, il faut parfois beaucoup de temps pour trouver l’erreur.

Un débogueur est un outil permettant d’exécuter un programme en l’arrê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 qu’une variable n’ait pas la valeur qu’elle devrait avoir ou que l’exé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 d’un 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 l’essentiel, les notes qui suivent sont une traduction de la section A Sample DDD Session de la documentation officielle.

Une session DDD simple

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 l’exemple 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. D’abord, nous devons compiler notre programme en vue de le déboguer, c’est à dire en incluant l’option « -g »1 :

$ gcc -g sample.c -o sample 

Maintenant nous pouvons activer DDD sur l’exé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 l’information 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 c’est de placer un point d’arrêt (ou breakpoint), qui fera s’arrêter sample à l’endroit qui nous intéresse. Cliquez dans l’espace blanc à gauche de l’initialisation de a. Le Argument field (): contient maintenant la position (sample.c:31). Maintenant, cliquez sur Break pour créer un point d’arrêt à l’emplacement dans (). Vous voyez un petit signal stop rouge apparaître dans la ligne 31.

La prochaine chose à faire est d’exécuter effectivement le programme, afin d’examiner 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é -- c’est à dire 8000 7000 5000 1000 4000. Cliquez sur Run pour démarrez l’exécution avec les arguments que vous venez de saisir.

Maintenant GDB fait démarrer sample. L’exécution stoppe au bout d’un instant, lorsque le point d’arrê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 d’une 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 n’est 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 d’abord 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 n’a 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 l’opé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 d’utiliser 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 l’affectation 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 l’exé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 l’appel 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 l’appel de shell_sort. DDD s’arrê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 -- c’est-à-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 l’initialisation de a et vous rendre directement à l’appel de shell_sort. Effacez l’ancien point d’arrêt en le sélectionnant et en cliquant sur le bouton Clear. Ensuite créez un nouveau point d’arrêt dans la ligne 35 juste avant l’appel de shell_sort. Pour exécuter le programme de nouveau, sélectionnez Program => Run Again.

DDD s’arrête avant l’appel 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 à l’interieur de l’appel 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 » (qu’on pourrait traduire par affichage de la structure locale). Cela montre un résumé de la pile d’exécution. A tout moment vous pouvez utiliser Status => Backtrace pour voir où en est la pile d’exécution (c’est-à-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! D’où 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 l’exé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! L’affichage 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 l’exécution restante du programme. Cliquez sur Cont pour continuer l’exé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 s’exé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 l’appel 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) 

C’est tout. Le programme est correct maintenant. Vous pouvez terminer la session DDD par la commande Program => Exit ou bien les touches Ctrl+Q.


Notes

1. L’option « -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. C’est sur l’appel d’une fonction que les boutons Step et Next différent : Step permet d’examiner l’intérieur de la fonction, s’arrêtant sur sa première ligne, tandis que Next considère un appel de fonction comme une instruction atomique dont il n’est pas utile d’examiner l’intérieur.