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 :
|
#include <stdio.h> #include <errno.h> int main (int argc, char **argv) { if (! (p = (int *) malloc(10*sizeof(int))))
{ |
(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.
![]() |
|
A titre de référence, voici la liste des variables d'environnement succeptibles d'être utilisées pour modifier le comportement d'Electric Fence :
EF_DISABLE_BANNER
Si cette variable a une valeur différente de zéro,
alors Electric Fence n'affichera pas de message lors de son démarrage.
L'utilisation de cette fonctionnalité est extrêmement déconseillée
si vous linkez votre programme statiquement, car vous risquer d'oublier d'enlever
Electric Fence dans la version en production de votre programme, avec les problèmes
de performances que cela comporte.
EF_PROTECT_BELOW
Par défaut (comme le montre la figure 1 qui ne devrait
pas être bien loin si elle ne s'est pas perdue au département PAO),
Electric Fence bloque l'accès à la page qui suit la zone allouée.
Par conséquent si l'on tente d'accéder en dehors de la zone allouée
par le bas, le problème ne sera pas détecté. Pour palier
à ce problème, lorsque la variable EF_PROTECT_BELOW a
une valeur non nulle, le mécanisme inverse est utilisé, c'est-à-dire
que c'est la première page qui est bloquée, et la zone mémoire
allouée est placée au début de la deuxième page
(voir la Figure 2).
![]() |
|
EF_PROTECT_FREE
Si cette variable est positionnée, Electric Fence
ne retournera pas la mémoire désallouée avec free() au
système, mais au contraire, il la rendra totalement inaccessible au programme.
Cette fonctionnalité est très utile si vous suspectez que le programme
que vous tentez de débugger essaye d'accéder à des zones
mémoire précédemment désallouées. Attention,
cette fonctionnalité décuple les besoins en mémoire de
l'application, vu que la mémoire allouée est rendue inutilisable.
EF_ALLOW_MALLOC_0
Par défaut, Electric Fence intercepte les appels à
la fonction malloc() avec une taille nulle, car ils sont le plus souvent dus
à un bug (quel pourrait bien être l'intérêt d'allouer
une zone de taille nulle?!). Cependant cette fonctionnalité peut être
désactivée en donnant une valeur non-nulle à la variable
d'environnement EF_ALLOW_MALLOC_0.
EF_ALIGNMENT
Ce paramètre permet de changer la valeur de l'alignement
des zones mémoire retournées par Electric Fence. En quoi est-ce
important? Tout simplement parce que par défault, les tailles des zones
mémoires sont des multiples de la taille des mots mémoire, soit
4 octets (32 bits) sur la plupart des architectures ou 8 octets (64 bits) sur
les Alpha et UltraSparc. Etant donné que chaque zone mémoire est
un multiple de 4 octets, un dépassement de moins de 3 octets ne sera
pas intercepté par Electric Fence.
Pour vous convaincre (si si, je vois bien à votre air dubitatif que vous
n'étes pas totalement convaincu), modifions l'exemple eftest1.c précédent
de la manière suivante :
|
#include <stdio.h> #include <errno.h> int main (int argc, char **argv) { if (! (p = (char *) malloc(10*sizeof(char))))
{ |
Comme vous pouvez le voir, la seule modification a consisté à
remplacer le tableau d'"int" par un tableau de "char".
Maintenant, compilons et exécutons ce programme :
# gdb -q ./eftest2
(gdb) set environment LD_PRELOAD=libefence.so.0.0
(gdb) run
Starting program: /home/vincent/articles/./eftest2
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Fin du test
Program exited normally.
Surprise... Cette fois l'erreur de dépassement n'a pas été
trouvée par Electric Fence, car lorsque l'allocation de 10 octets a été
demandé, c'est en fait un nombre entier de mots machine de 4 octets qui
ont été alloués (3 dans notre exemple), ce qui nous fait
12 caractères. Le code de l'exemple n'accédant qu'aux 11 premiers,
aucune erreur n'a été détectée. Par contre, si l'on
tente d'accéder au delà de ces 12 octets, alors l'erreur sera
interceptée; vous pouvez le tester par vous-même en remplaçant
le "10" de la boucle "for" par "12".
Alors comment faire pour détecter les problèmes d'allocation mémoire
dont la taille est inférieure à la taille d'un mot machine? Heureusement,
Electric Fence fourni un mécanisme pour contourner ce problème
: la variable EF_ALIGNMENT peut être utilisée pour définir
sur combien d'octets l'alignement des zones allouées sera fait. Pour
détecter les dépassements d'un seul octet, il suffit donc de positionner
cette variable à 1. Pourquoi n'est ce pas fait par défaut ? Tout
simplement à cause du fait que la fonction malloc() est sensée
retourner une zone mémoire alignée sur un mot machine et que tous
les processeurs ne sont pas capables d'accéder à des données
qui ne respectent pas scrupuleusement cette contrainte d'alignement (les processeurs
Alpha par exemple).
(gdb) set environment EF_ALIGNMENT=1
(gdb) run
Starting program: /home/vincent/articles/./eftest2
Electric Fence 2.0.5 Copyright (C) 1987-1998 Bruce Perens.
Program received signal SIGSEGV, Segmentation fault.
0x804857c in main (argc=1, argv=0xbffffcc4) at eftest2.c:13
13
p[i]=i;
(gdb)
Cette fois, en enlevant la contrainte d'alignement, le problème est
bien intercepté.
|
Lorsqu'Electric Fence est utilisé pour débugger un programme,
les besoins en mémoire de celui-ci sont décuplés, et
il se peut que vous atteignez rapidement les limites de la mémoire
virtuelle de votre machine. Si vous rencontrez ce problème, il est
facile d'y remédier temporairement grâce à un fichier
de swap supplémentaire (oui : un fichier, pas une partition) Supposons que vous estimiez à 80 Mo votre besoin de mémoire supplémentaire. Il vous suffit de : 1 - Créer un fichier vide de 80 Mo : # dd if=/dev/zero of=/tmp/extraswap count=80 bs=1M 80+0 records in 80+0 records out 2 - Le "formatter" avec la commande mkswap : # mkswap /tmp/extraswap Setting up swapspace version 1, size = 83881984 bytes 3 - Monter le fichier créé : # swapon /tmp/extraswap A ce stade, vous pouvez utiliser la commande 'free' pour vérifier
que la taille de votre espace de swap a bien augmenté de 80 Mo.
Lorsque vous n'aurez plus besoin de ce swap supplémentaire, vous
pourrez le désactiver grâce à la commande 'swapoff
/tmp/extraswap', puis effacer le fichier. |
strace & ltrace
Ces 2 outils ne sont pas aussi génériques que ceux que nous venons de présenter, et leur intérêt est souvent plus limité pour débugger un programme dont vous êtes l'auteur. Cependant, ils ont pour particularité (à la différence des outils présentés jusqu'à présent) d'être extrêmement utiles pour déterminer ce qui se passe dans un programme pour lequel vous ne disposez pas du code source.
strace
Cet outil permet de visualiser les appels systèmes
effectués par le programme tracé. Notons au passage que le système
Solaris comporte également un outil similaire, nommé 'truss' dans
les anciennes versions et 'trace' dans les versions plus récentes.
Essayons strace sur le programme d'exemple eftest2 dont nous avons
donné le code source un peu plus haut :
# strace ./eftest2 execve("./eftest2", ["./eftest2"], [/* 25 vars */]) = 0 brk(0) = 0x8049840 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40016000 open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=38822, ...}) = 0 old_mmap(NULL, 38822, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000 close(3) = 0 open("/lib/libc.so.6", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0755, st_size=1057576, ...}) = 0 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\224\314"..., 4096) = 4096 [...] brk(0x8049cb8) = 0x8049cb8 brk(0x804a000) = 0x804a000 [...] ioctl(1, TCGETS, {B9600 opost isig icanon echo ...}) = 0 write(1, "Fin du test\n", 12) = 12 [...] close(3) = 0 munmap(0x40017000, 4096) = 0 _exit(0) = ? # |
Outre les premières lignes qui correspondent à l'initialisation du processus et des bibliothèques dynamiques qu'il utilise, on voit les appels à brk (appel système utilisé par la fonction malloc) et à write (correspondant à la fonction printf).
Ce type d'outil est par exemple très utile si l'on cherche à connaître les fichiers d'initialisation lus par un programme, auquel cas il suffira de rechercher les occurences de l'appel système 'read'.
ltrace
Le fonctionnement de ltrace est relativement similaire à celui
de strace, à la différence près que ltrace montre
non pas les appels systèmes, mais les appels aux fonctions de bibliothèques.
Voici par exemple les informations fournies par ltrace sur le même programme
d'exemple.
# ltrace ./eftest2 __libc_start_main(0x08048610, 1, 0xbffffd14, 0x080483ec, 0x080486dc <unfinished ...> __monstartup(0x080484f0, 0x080486f8, 0xbffffcb8, 0x4004f4a8, 0x401221e8) = 0 atexit(0x08048470) = 0 __register_frame_info(0x08049730, 0x08049828, 0xbffffcb8, 0x4004f4a8, 0x401221e8) = 0x40122e60 mcount(0x08049744, 0xbffffcc8, 0xbffffce8, 0x4003dbcc, 1) = 0x40120a58 malloc(10) = 0x08049cb0 printf("Fin du test\n"Fin du test ) = 12 exit(0) = <void> _mcleanup(0x401221e8, 0x40015ec0, 1, 0, 0x4004f360) = 0x08049ca8 __deregister_frame_info(0x08049730, 0x400163f8, 0xbffffc6c, 0x400e8121, 0x08049848) = 0x08049828 +++ exited (status 0) +++ # |
La plupart des appels mis en évidence sont en fait internes au fonctionnement de la libc, mais l'on voit tout de même les appels des 2 fonctions de bibliothèques faits par notre programme : malloc() et printf().
Maintenant, non seulement, vous n'avez plus aucune excuse pour avoir des fuites
mémoire dans vos programmes, mais vous étes également armés
pour corriger les bugs de tout autre programme que vous pourrez être amené
à utiliser.
|
GNU gdb : http://sources.redhat.com/gdb/ Electric Fence : Disponible en paquet Debian sous le nom 'electric-fence'. Sources : ftp://ftp.perens.com/pub/ElectricFence/ KDevelop : http://www.kdevelop.org/ gcc-checker : http://www.gnu.org/software/checker/checker.html glib : ftp://ftp.gtk.org/pub/gtk/v1.2/ strace : http://www.wi.leidenuniv.nl/~wichert/strace/ ltrace : ftp://ftp.debian.org/debian/dists/unstable/main/source/utils/ltrace_0.3.10.tar.gz |