Le langage C
Quatrième partie
par Guilhem de Wailly
Dans cette série d'articles, nous présentons le langage C. Il ne s'agit pas de réécrire les ouvrages de référence donnés en annexe, mais de donner, au travers du C, une méthode de programmation basée sur la notion d'interface.
Au fur et à mesure de notre progression, nous décrirons les éléments indispensables du langage et conduirons le lecteur à consulter les ouvrages de références. Le but poursuivit est clairement de parvenir à construire des programmes de manière segmentée.
Dans cet article, nous discutons de la portée des variables, de la déclaration et de la définition des fonctions C.
Cette présentation est basée sur les outils GNU comme gcc, ld et ar et le débogueur gdb. Elle peut etre transposée dans d'autres environnements comme les plates-formes Microsoft, bien que les outils y soit souvent complètement intégrés, confisquant du meme coup la souplesse, la puissance et la liberté à l'utilisateur.
Ce mois-ci, nous allons nous intéresser aux débogueurs que Linux nous propose. Tous les débogueurs qui traitent des binaires compilés avec le compilateur C gcc sont basés sur le débogueur GNU gdb.
Pour nous aider à déboguer, il nous faut un programme. Nous reprenons le dernier programme présenté dans le précédent Linux Magazine :
/* FICHIER ex.c */ #include <stdio.h> /* déclaration des fonctions */ double f (double a); /* format long */ double g (double); /* format court */ /* définition des fonctions */ double f (double a) { printf ("a = %f\n", a); if (a <= 0.01) { return (0.0); } else { return g (a * 2.0); } } double g (double x) { printf ("x = %f\n", x); /* syntaxe du if sans accolades */ if (x <= 0.01) return (0.0); else return f (x / 3.0); } /* programme principal */ void main (void) { f (10); }
Ce programme est composé de plusieurs fonctions, main, g et f. La fonction main est le point d'entrée du programme : c'est par elle que va commencer l'exécution.
Les deux premières lignes de code sont des prototypes des fonctions, afin de résoudre le problème de la récursion mutuelle entre les fonctions f et g (le fait que f utilise g et que g utilise f).
Ce programme va nous servir d'exemple pour utiliser les différents débogueurs présentés dans cet article.
Pour pouvoir déboguer un programme, il faut le compiler avec une option spécifique qui ajoute des informations au binaire produit. Ces informations sont regroupées dans une table appelée table des symboles.
La table des symboles permet de retrouver le nom du fichier source et la ligne de code concernée à partir d'une position dans l'exécutable.
Les binaires compilés avec l'option de débogage sont plus volumineux est plus lent que lorsqu'ils sont compilés sans cette option.
L'option de débogage est introduite dans gcc par -g. Nous compilons donc notre programme ex.c avec :
$ gcc -g ex.c
Cette commende fabrique un fichier a.out qui est le binaire produit à partir du programme source contenu dans le fichier ex.c. On peut produire un fichier en spécifiant explicitement un autre nom en utilisant l'option -o fichier.
Gdb est un débogueur "ligne de commande", c'est à dire qu'il se pilote en tapant les commandes sur le prompt. Bien que soient interface utilisateur est très primitive, le débogueur gdb est extrèmement puissant.
Le fait que cette interface soit très simple peut aussi rendre service lorsque l'on souhaite d`éboguer un programme via un terminal lent. Avec un peu d'habitude, finalement, ce n'est pas si mal !
Pour commencer la session de débogage, il faut invoquer gdb avec le nom du programme binaire :
$ gdb a.out gdb a.out GNU gdb 4.17.0.4 ... (gdb)
Nous sommes maintenant dans le débogueur. Le système est en attente et le programme n'est pas encore en cours d'exécution. Pour l'exécuter, nous tapons :
(gdb) run Starting program: a.out a = 10.000000 x = 20.000000 ... a = 0.006766 Program exited with code 040. (gdb)
Le système se retourne dans le meme état que précédemment. Le débogueur ne nous a pas beaucoup servit, non ?
Pour y voir quelque chose, il est nécessaire de placer un point d'arret. Nous allons placer un point d'arret sur la fonction main :
(gdb) break main break main Breakpoint 1 at 0x8048517: file ex.c, line 29 (gdb)
Le système nous confirme que le point d'arret est bien positionné. On constate aussi qu'il sait parfaitement où est la fonction main dans le programme source, grace à la table des symboles. Recommençons maintenant l'exécution :
(gdb) run Starting program: a.out Breakpoint 1, main () at ex.c:29 29 f (10); (gdb)
Cette fois-ci, le système nous indique qu'il a rencontré un point d'arret dans le fichier ex.c, ligne 29.
Il est possible de placer des points d'arret par rapport à une position dans un fichier et de placer une condition ou un compteur.
Pour afficher la liste des points d'arret, on utilise la commande break sans arguments :
(gdb) break Breakpoint 2 at 0x8048476: file ex.c, line 11. (gdb)
Pour effacer l'ensemble des points d'arrêt, utiliser la commande delete. Pour effacer le point d'arret où le système est actuellement positionner, utiliser clear sans arguments.
Nous sommes donc arrétés sur le point d'arret de la fonction main. Nous voulons afficher le les lignes de code qui entourent la position courante :
(gdb) list 24 else return f (x / 3.0); 25 } 26 27 /* programme principal */ 28 void main (void) { 29 f (10); 30 } (gdb)
Cela permet souvent de savoir où on en est !
Bon, avançons d'une ligne :
(gdb) next a = 10.000000 x = 20.000000 ... a = 0.006766 (gdb)
Ah, on est allé trop vite ! En effet, la commande next fait avancer d'un pas, sans entrer dans les fonctions. Pour entrer dans les fonctions, il faut utiliser la commande step. Exécutons à nouveau le programme :
(gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/tmp/a.out Breakpoint 1, main () at ex.c:29 29 f (10); (gdb)
Nous voyons que la session de debogage precedente n'est pas terminee. Mais gdb supporte parfaitement le redémarrage. Un point important : le programme est exécuté exactement au meme emplacement en mémoire. Cela permet d'utiliser des références en mémoire absolues, lorsque c'est nécessaire.
Maintenant, utilisons la commande step pour entrer dans la fonction f au lieu de l'exécuter :
(gdb) step f (a=10) at ex.c:11 11 printf ("a = %f\n", a); (gdb)
Maintenant, nous sommes à l'intérieur de la fonction f. Nous pouvons poursuivre le débogage à l'aide de next, step et run, autant de fois que nous le souhaitons. Notons que les commandes on une syntaxe courte. Par exemple, la commande step peut etre activée avec la lettre s seule, next avec n et run avec r.
En tout point de notre exécution en mode pas à pas, le débogueur garde une trace des appels de fonctions précédents. Chaque appel est stocké dans le débogueur dans un cadre d'exécution (execution frame). Pour afficher les cadres, utilisons bt (backtrack) :
(gdb) bt #0 f (a=10) at ex.c:11 #1 0x8048523 in main () at ex.c:29 (gdb)
Cet affichage nous indique la liste des fonctions qui sont en cours d'exécution, par rapport à l'endroit du programme où nous sommes.
Gdb permet de circuler parmi les cadres, avec les commandes up et down. La première "remonte" d'un cadre (vers main, dans notre cas) et la seconde ``descend'' d'un cadre :
(gdb) up #1 0x8048523 in main () at ex.c:29 29 f (10); (gdb)
Nous sommes maintenant dans le cadre de main. Nous pouvons examiner les variables, les modifier, puis nous revenons dans le cadre de f avec :
(gdb) down #0 f (a=10) at ex.c:11 11 printf ("a = %f\n", a); (gdb)
Il est aussi très utile d'afficher le contenu d'une variable. Pour cela, on utilise la commande print :
(gdb) print a $1 a= 10 (gdb)
Il est possible de modifier le format d'affichage :
(gdb) print/x a $1 a= 0xa (gdb)
Cette dernière commande affiche la variable a en hexadécimal. On peut utiliser les types c (caractère), x (hexadécimal), o (octal), d (décimal).
Parfois, on souhaite afficher en permanence une variable, à chaque étape de l'exécution. Pour cela, on utilise la commande display :
(gdb) display a 1: a = 10 (gdb)
Maintenant, à chaque pas de l'exécution, la valeur de a sera affichée. Gdb n'affiche les variables de la sorte que aux endroits du programme où elles sont accessibles.
Gdb dispose d'une aide en ligne, pas très facile à utiliser. Elle est invoquée avec :
(gdb) help List of classes of commands: aliases -- Aliases of other commands breakpoints -- Making program stop at certain points ... Type "help" followed by a class name for a list of commands in that class. Type "help" followed by command name for full documentation. Command name abbreviations are allowed if unambiguous. (gdb)
Une aide plus complète est plus facile d'accès est disponible dans l'éditeur emacs. Nous préférons utiliser xemacs, plus ergonomique.
La première chose à connaitre dans emacs est la manière d'appeler l'aide. Une fois l'éditeur lancé, taper sur la touche [ECHAP] puis sur la touche [X] : cela active la ligne d'édition en bas de l'écran. Taper alors info puis [ENTER]. Nous avons alors l'écran suivant :
Fig 1 : Aide hypertexte de emacs.
Une fois dans l'aide hypertexte, rechercher la chaine de caractères gdb (en tapant [CTRL]+[S], puis gdb), se placer au début de la ligne et presser [ENTER] : Nous sommes maintenant dans l'aide de gdb, très complète.
Cette aide est très utile pour compléter ses connaissances de gdb. Elle traite aussi de nombreux autres sujets, comme emacs lui-meme, les fonctions C (libc), la gestion des projets (make), la gestion de version (Source Control), etc.
Chargeons maintenant le fichier ex.c dans emacs en tapant [CTRL]+[F] ou en ouvrant le menu File->Open. Lorsque l'on utilise les touches, la touche [TAB] sert à compléter la ligne avec les choix possibles. Le caractère ~ est remplacé par le nom du répertoire de l'utilisateur.
Lorsque le fichier est chargé, on constate avec bonheur que emacs reconnait la syntaxe du C et colorise le fichier.
Dans emacs, en mode édition, la touche [TAB] n'insère pas de tabulation, mais indente la ligne correctement, en fonction du contexte : C'est très pratique pour écrire des programmes toujours impécables !
Pour compiler un programme à l'intérieur de emacs, rien de plus simple : Appuyer sur le bouton [Compile] de la barre d'outils, puis sur le bouton [Edit Command].
En base de l'écran, taper alors la commande de compilation utilisée précédemment, gcc -g ex.c. Emacs ouvre un second cadre contenant le résultat de la compilation.
Dans la figure suivante, nous avons volontairement provoqué une erreur en ajoutant la seconde ligne :
Fig 2 : Compilation dans emacs.
En cliquant sur le message d'erreur, emacs nous place directement dans le fichier source ayant provoqué l'erreur, à la bonne ligne. Pas mal, non ?
Avec la commande de compilation make, emacs permet de compiler des projets qui peuvent etre volumineux. Il est aussi possible de gérer les versions des sources (regardez à Version Control dans l'aide info).
En fait, emacs se comporte comme un véritable environnement de programmation. Mais sa force vient du fait qu'il est capable de lancer n'importe quelle commande de compilation, et dont de compiler n'importe quoi.
Si vous l'avez ajoutée, enlevez la seconde ligne et recompilez votre programme.
Maintenant, nous allons déboguer le programme sans quitter emacs ! Pour cela, cliquer sur le bouton [Debug] de la barre d'outils. La boite de dialogue qui s'ouvre demande le nom du binaire à déboguer : Le binaire s'appelle a.out.
Et voila, nous sommes dans le débogueur de emacs, comme on le voit dans la figure suivante :
Guilhem de Wailly (gdw at free dot fr)