Les
outils pour débugger sous Linux
Les
programmeurs qui développent en C ou C++ ont tous passé de longs
moments à débugger leurs programmes. Cet article présente les
principaux outils disponibles sous Linux pour les aider dans cette
tâche fastidieuse. En plus des outils "classiques" comme
GNU gdb ou DDD, des outils plus spécialisés comme
Electric Fence sont également présentés. Bien que cet article
s'adresse en priorité aux programmeurs débutants, les vieux routards
pouront également y trouver leur bonheur.
GNU gdb
GNU
gdb est LE programme de débuggage sous Linux (ainsi que sur
bon nombre d'autres plate-formes). En dépis de son interface en
mode texte, bien qu'il existe des surcouches graphiques, comme
DDD ou KDevelop que nous verrons plus loin, il est extrêmement
puissant et permet de venir à bout des bugs les plus corriaces
et les plus incrustés dans vos programmes.
Présentation
générale :
Un
débugueur comme gdb permet de visualiser ce qui se passe dans
votre programme, soit au cours de son exécution, soit au moment
où il a crashé dans le cas où gdb est utilisé pour faire une analyse
post-mortem (nous reviendrons sur ce terme plus loin).
gdb permet quatre principaux types d'actions pour déceler les
problèmes :
- Lancer un programme en spécifiant les paramètres appropriés.
- Demander au programme de s'arrêter dans des circonstances bien
précises.
- Examiner ce qui s'est passé, une fois le programme terminé.
- Changer l'état d'exécution du programme (comme la valeur d'une
variable) pour corriger les résultats.
La version actuelle de gdb (5.0) permet de débugger les programmes
écrits en C, C++, Modula-2, Chill (un "dialecte" du
LISP), Fortran et Java. Il existe également des patchs pour supporter
l'ADA et l'Objective-C.
Notons au passage que pour être pleinement exploitable par gdb,
un programme doit être compilé avec l'option '-g', voire '-g3';
d'autre part les optimisations (-O/-O2/etc) doivent être désactivées
dans la mesure du possible.
Pour
lancer gdb, il suffit de tapper 'gdb' au prompt shell,
mais le plus souvent on lance gdb avec quelques paramètres : soit
'gdb ./monprog' pour spécifier directement le programme
cible (ce qui évite de taper 'file ./monprog' dès l'apparition
du prompt gdb), soit 'gdb ./monprog core' pour faire
une analyse post-mortem à partir d'un fichier core. Il est également
possible d'utiliser gdb sur un programme qui est déjà en cours
d'exécution : 'gdb ./monprog 2345' pour attacher gdb
au processus dont le PID est 2345. Attention, gdb recherche les
fichiers core avant les PID, dans ce dernier cas, vous ne devez
pas avoir de fichier core ayant le même nom que le PID
du processus dans le répertoire courant.
Voici
la liste des commandes gdb les plus utilisées :
break
[fichier_source:]fonction
Met un point d'arrêt au début de la fonction
spécifiée. Lorsque l'exécution du programme rencontre un de ces
points d'arrêt, le programme est stoppé et le programmeur peut
entrer d'autres commandes au prompt du débogueur. L'exécution
pourra ensuite être reprise grâce à la commande 'cont'.
A la place d'un nom de fonction, on peut directement passer un
numéro de ligne dans le code source à la commande 'break'.
condition
numéro test
Permet de poser une condition sur un point
d'arrêt précédement défini par la commande 'break'. Par
exemple, 'condition 1 i==10' n'activera le point d'arrêt
numéro 1 que dans le cas où la variable i vaut 10.
run
[liste de paramètres]
Démarre l'exécution du programme avec les éventuels
paramètres spécifiés.
bt
Backtrace : demande à gdb d'afficher le contenu
de la pile du programme. Synonyme de la commande 'where'.
print
expression
Affiche la valeur d'une expression. En particulier,
cette commande peut être utilisée pour afficher la valeur d'une
variable.
cont
Demande la reprise de l'exécution d'un programme
stoppé.
next
Exécute la ligne suivante dans un programme
suspendu. Si cette ligne contient des appels de fonctions,
elles seront exécutées sans être elles-mêmes tracées.
nexti
Idem que la commande 'next', mais
le pas de progression n'est plus la ligne de programme, mais l'opcode
assembleur (commande à réserver pour les cas les plus désespérés).
set
[option] [paramètres]
Cette option permet de changer la plus grande
partie des paramètres de gdb ; parmi les principales options disponibles
:
environment : permet de changer une
variable d'environnement du programme exécuté.
language : permet de changer le langage
source par défaut.
variable : permet de changer à la
volée le contenu d'une variable.
step
Idem que la commande 'next', mais
les éventuelles fonctions appelées seront elles aussi tracées.
stepi
Idem que la commande 'step', mais
le pas de progression n'est plus la ligne de programme, mais l'opcode
assembleur.
help
[nom]
Donne des informations sur la commande donnée
en paramètre, ou une page d'aide générique si aucun paramètre
n'est mentionné.
quit
Quitte gdb.
Il
est important de noter que la ligne de commande de gdb utilise
la bibliothèque GNU readline et que par conséquent tous les raccourcis
clavier courrants (^A, ^E, ^K, etc...) ainsi que le mécanisme
d'historique (flèches curseur, ^R) sont disponibles.
En
cas de gros problème, voire de doute sur le code généré par le
compilateur, gdb permet également de désassembler le code d'une
fonction. Bien évidemment, il est rare d'en arriver à une telle
extrémité ;-)
(gdb) disassemble Double
Dump of assembler code for function Double:
0x8048480 <Double>: push
%ebp
0x8048481 <Double+1>: mov
%esp,%ebp
0x8048483 <Double+3>: mov
0x8(%ebp),%eax
0x8048486 <Double+6>: add
%eax,%eax
0x8048488 <Double+8>: leave
0x8048489 <Double+9>: ret
End of assembler dump.
DDD & KDevelop
DDD
et KDevelop sont les 2 interfaces graphiques les plus pratiques
de gdb. Par rapport à KDevelop, DDD fait figure d'ancétre, mais
celui-ci est très convivial, donc très simple à utiliser et peu
gourmand en ressources.
DDD : Fenêtre principale
Cependant,
DDD ne reste qu'une interface graphique au dessus de gdb, ce qui
pourra laisser les plus exigeants sur leur faim. Heureusement,
KDevelop comble un manque important : c'est un véritable environnement
de développement intégrant un éditeur avec colorisation du code,
un système de gestion de projets et enfin une interface de débugage.
Bien que KDevelop soit une application KDE, elle fonctionne très
bien avec simplement les bibliothèques Qt et KDElibs, c'est à
dire que vous n'aurez pas toutes les applications KDE à installer
si vous désirez utiliser KDevelop; seules les bibliothèques de
base sont suffisantes. D'autre part, KDevelop permet sans aucun
problème de développer et débugguer des applications GNOME ;-)
Bref, si la ligne de commande de gdb vous rebute, si vous ne maîtrisez
pas totalement Emacs et que vous ne supportez pas vi,
alors KDevelop est certainement fait pour vous. KDevelop intéressera
également les développeurs qui découvrent le monde Linux alors
qu'ils étaient habitués aux environnements de développement intégrés
du monde Windows.
Les problèmes de plomberie
Vous
avez déjà probablement eu a retrouver des "fuites" de
mémoires, c'est-à-dire à trouver des zones mémoires qui ont été
allouées, mais qui ne sont pas désallouées par la suite. Dans
cette partie, nous allons faire un tour d'horizon des divers outils
disponibles sous Linux qui peuvent aider dans cette recherche.
Vous
savez déjà probablement qu'une zone de mémoire allouée est automatiquement
restituée au système d'exploitation lors de la terminaison du
processus. Dans ce cas, pourquoi s'obstiner à rechercher ces fuites
? Celles qui sont situées dans les programmes qui n'allouent dynamiquement
que peu de mémoire et dont le temps de fonctionnement est très
cours (comme par exemple, les commandes ls ou ps)
ne prettent pas à conséquences bien que ce soit un style de programmation
relativement peu esthétique. Par contre, si des fuites surviennent
dans des programmes qui utilisent beaucoup de mémoire ou qui sont
sensés tourner pendant longtemps, alors ce problème devient réelement
génant. Lequel d'entre vous n'a pas déjà eu à redémarrer Netscape
ou son serveur X (voire les deux) parce qu'il occupait plus de
100 Mo en mémoire ?
Les
fuites ne sont pas les seuls problèmes liés à l'allocation mémoire.
Les langages C et C++ n'effectuant pas de tests pour vérifier
si l'on accéde bien toujours à des zones mémoires correctes, qu'elles
aient été allouées dynamiquement (par un appel à la fonction malloc())
ou non (tableau statique). Dans ce cas, cela se terminera dans
la plupart des cas par un résultat erroné ou par une erreur de
segmentation (voire l'un suivi de l'autre).
Heureusement, il existe sous Linux quelques outils qui permettent
de grandement simplifier la chasse aux bugs.
Electric Fence
Electric
Fence (litéralement "cloture électrique") est un outil
très simple, mais il permet néanmoins d'intercepter un grand nombre
de problèmes grâce à un minimum d'efforts. Plus particulièrement,
Electric Fence rend possible la détection des erreurs suivantes
:
- accès en dehors des zones allouées par la fonction malloc()
("buffer overruns" et "buffer underruns")
- accès à une zone mémoire retournée au système par un appel à
la fonction free().
- détection des problèmes d'alignement.
A la différence des autres outils similaires, Electric Fence détecte
non seulement les tentatives d'écritures en dehors des zones allouées,
mais également les tentatives de lecture. C'est, en grande partie,
ce qui fait l'intérêt de cet outil.
Electric
Fence se présente sous la forme d'une bibliothèque avec laquelle
vous devrez "linker" votre programme. Vous pouvez
choisir d'utiliser la bibliothèque libefence explicitement lors
de l'édition de liens finale en ajoutant l'option '-lefence'
lors de l'appel à la commande gcc, cependant il est beaucoup plus
pratique de forcer l'édition de liens au moment de l'exécution
grâce à la variable d'environnement LD_PRELOAD. Cela
peut se faire simplement à partir de la ligne de commande :
# LD_PRELOAD=libefence.so.0.0 ./monprog
ou bien depuis gdb grace à la directive 'set environment LD_PRELOAD=libefence.so.0.0'.
Voyons
concrétement avec un exemple comment s'utilise Electric Fence.
Considérons le petit programme suivant :
eftest1.c |
#include
<stdio.h>
#include <errno.h>
int
main (int argc, char **argv) {
int *p, i;
if (! (p = (int *) malloc(10*sizeof(int)))) {
perror("echec
de malloc:");
exit(-1);
}
for (i=0; i<=10; i++) {
p[i]=i;
}
printf("Fin du test\n");
exit(0);
}
|
(Les
lecteurs attentifs auront certainement déjà vu qu'il y à une erreur
dans le code, mais merci de ne rien dire pour le moment afin de
ne pas me gacher mon effet de surprise que je réserve pour plus
tard ;-)
On
compile ce programme grâce à la commande :
$ gcc -g eftest1.c -o eftest1
puis on l'exécute :
$ ./eftest1
Fin du test
$
Tout
s'est donc passé comme prévu ; du moins en apparence, car cela
ne veut pas dire que le programme fonctionne correctement. Pour
nous en convaincre relançons ce même programme, mais cette fois
en utilisant Electric Fence :
$ LD_PRELOAD=libefence.so.0.0 ./eftest1
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Segmentation fault.
$
On
note au passage qu'Electric Fence a bien été trouvé comme l'indique
le message (Ne vous laissez pas impressionner par le fait que
la dernière version d'Electric Fence date de 1998; cela est tout
simplement dû au fait que cet utilitaire remplit parfaitement
son rôle et qu'aucun bug n'y a été découvert depuis cette date).
Mais on voit surtout que le programme a planté avant la fin à
cause d'une erreur de segmentation. Pour y voir plus clair revoyons
la scène avec gdb (à défaut de ralenti).
$
gdb -q ./eftest1
(gdb) set environment LD_PRELOAD=libefence.so.0.0
(gdb) run
Starting program: /home/vincent/articles/./eftest1
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Program
received signal SIGSEGV, Segmentation fault.
0x8048520 in main (argc=1, argv=0xbffffd44) at eftest1.c:12
12 p[i]=i;
(gdb) print i
$1 = 10
A
ce stade, on en sait déjà un peu plus. D'une part, on sait précisément
quelle ligne du programme a provoquée l'erreur ; d'autre part,
on a pu déterminer que la variable i valait 10 au moment
du plantage. A partir de là, l'erreur est évidente : l'appel de
malloc() a alloué un segment de 10 mots machine, alors
que la boucle for essaye d'en utiliser 11 (de 0 à 10, cela
fait bien 11, pas 10).
Pour
cet exemple, on a utilisé le mode de fonctionnement par défaut
de Electric Fence, à savoir les problèmes de dépassements par
le haut (buffer overruns). Du fait de sa conception, Electric
Fence ne permet pas de détecter en une seule exécution les dépassements
par le haut et par le bas.
En fait Electric Fence utilise le hardware gérant la mémoire virtuelle
(la MMU) pour placer une page inaccessible après (ou avant
selon l'option utilisée) chaque plage de mémoire allouée. De la
sorte, lorsque le programme tente de lire ou d'écrire cette page
inaccessible, une erreur de segmentation est déclenchée. A partir
de ce moment, il est trival de trouver l'instruction fautive grâce
à gdb, comme l'a montré l'exemple précédent. De même une zone
mémoire qui a été libérée par un appel de la fonction free() sera
rendue inaccessible, de sorte qu'une tentative d'accès après libération
provoque un plantage du programme.
En ce qui concerne son utilisation, Electric Fence peut être utilisé
pour simplement générer un fichier core qui sera utilisé
pour une analyse post-mortem; cette méthode est cependant déconseillée,
car du fait qu'Electric Fence alloue systèmatiquement 2 pages
mémoire (soit 8 Ko sur les architectures i386), le programme débugué
nécessitera une quantité de mémoire considérablement plus importante.
|