:: Enseignements :: Licence :: L2 :: 2011-2012 :: Système ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) | Locale, chargement dynamique et traceur d'allocations mémoire |
Exercice 1 - Au travail!
Ecrire un fichier message_fr_FR.c contenant une fonction void message(void)
affichant "Bonjour" sur la sortie standard.
Ecrire un fichier message_POSIX.c contenant une fonction void message(void)
affichant "Hello" sur la sortie standard.
À l'aide d'un Makefile, compiler ces deux fichiers sous forme de deux
bibliothèques dynamiques libmessage_fr_FR.so et libmessage_POSIX.so.
Ecrire un programme message.c qui consulte la valeur de la variable
d'environnement LANG (man getenv), charge la bibliothèque libmessage_$LANG.so
(man dlopen). Si celle-ci n'existe pas, charger la bibliothèque
libmessage_POSIX.so. Ensuite charger la fonction message (man dlsym)
dans cette bibliothèque et appeler cette fonction.
ATTENTION: la variable LANG peut être de la forme XXX.YYY, et dans ce cas,
YYY représente l'encodage à utiliser (UTF8, ...) et il ne faut donc prendre en compte
que XXX et essayer de charger la bibliothèque libmessage_XXX.so.
Exercice 2 - Attraper des fonctions de la libc
Le but de cette partie est le suivant: on veut garder une trace de chaque malloc,
calloc, realloc et free effectués dans un programme,
pour indiquer, à la fin du programme, si toute la mémoire allouée a bien été libérée
(et si de la mémoire a été libérée alors qu'elle n'a pas été allouée,
le dire tout de suite). Pour cela, on va devoir être capable d'intercepter les appels
à ces 4 fonctions pour les remplacer par des traitements personnalisés.
Note: Le traceur que nous allons écrire ne fonctionne pas avec les programmes dits multithread. Pour
savoir si un programme est multithread ou non, il suffit de lancer la commande ldd dessus et de
voir s'il existe une dépendance avec la bibliothèque libpthread. Par exemple on peut entrer
la commande ldd /bin/ls et constater que ce programme est multithread, tandis
que le programme /usr/bin/gcc ne l'est pas.
Les choses sérieuses commencent, car pour détourner malloc, nous aurons quand même besoin
d'avoir accès à la vraie fonction malloc. Or, on ne pourra pas obtenir celle-ci en chargeant
la libc avec dlopen, car dlopen alloue de la mémoire avec malloc,
ce qui nous conduirait à un cercle vicieux. Heureusement,
tout n'est pas perdu, grâce à une petite gymnastique (vue en cours):
l'utilisation de dlsym(RTLD_NEXT,symbole), qui va chercher le symbole voulu dans les
bibliothèques masquées. Attention: il faudra définir la macro _GNU_SOURCE
avant d'inclure dlfcn.h pour que ça marche (la macro RTLD_NEXT n'est pas
définie sinon). En fait, il faut que cette définition soit faite avant toute autre chose, y
compris l'inclusion de stdio.h. Le plus simple est donc de passer l'option
-D_GNU_SOURCE à gcc, ce qui garantira que la macro est définie avant tout le reste.
Vérifier que l'on arrive à récupérer les symboles de malloc, calloc, free et
realloc de cette manière, et les appeler chacun une fois (dans un ordre correct) avec un
petit programme de test. Tester avec valgrind pour être sûr que la mémoire est bien
allouée puis libérée.
Exercice 3 - Intercepter les appels à ces fonctions
On veut maintenant pouvoir remplacer les appels à ces fonctions par du code personnalisé.
Il faut pour cela écrire une bibliothèque (c'est-à-dire un programme sans main)
avec des fonctions malloc, calloc, free,
et realloc "maison".
Dans un premier temps, se contenter de faire un petit affichage (par exemple
"malloc a été appelé pour une taille de X octets, et a retourné le pointeur Y")
et appeler la "vraie" fonction malloc récupérée par dlsym comme vu
plus haut.
Indication: utiliser des variables
static pour stocker les pointeurs vers
les vraies fonctions, selon le modèle suivant:
void* malloc(size_t n) {
static (void*)(*real_malloc)(size_t);
if (real_malloc==NULL) real_malloc=/* mettre ici le code pour charger le vrai malloc */
return real_malloc(size);
}
Attention: dans l'ensemble de la bibliothèque, il n'est pas souhaitable d'utiliser les
fonctions d'affichage de la bibliothèque standard (printf, fprintf,
etc...). En effet, celles-ci utilisent des buffers internes gérés avec malloc.
C'est pourquoi vous devez utiliser les trois macros définies dans le fichier suivant:
La première macro (affiche_chaine) affiche une chaîne de caractère sur la
sortie erreur standard, la deuxième (affiche_hexa) affiche une valeur
(pointeur par exemple) en hexadecimal sur la sortie erreur standard, et la troisième
(affiche_entier) affiche une valeur entière.
Rappel: pour compiler vos fonctions en bibliothèques utiliser les options
-fpic -shared -ldl du compilateur.
Tester avec le programme "dico" du TD précédent. Tester aussi avec des commandes comme
find, grep ... Pour cela, faire comme ceci :
LD_PRELOAD=./allocateur.so programme-a-lancer
Le LD_PRELOAD va indiquer au linker dynamique de commencer par chercher
les fonctions "manquantes" dans ./allocateur.so avant de
piocher dans les autres bibliothèques dynamiques du système.
Exercice 4 - Traceur d'allocations mémoire
Maintenant, coupler tout ceci avec les tables de hachage des TD précédents. À chaque fois qu'une
allocation sera faite, il faudra ajouter dans la table de hachage l'adresse et la taille de la zone
qui a été allouée, pour la suivre à la trace lors des libérations et réallocations. Pour cela,
vous modifierez le type any utilisé dans le TP sur les tables de hachage:
struct addr_info {
void* ptr;
size_t size;
};
typedef union {
void* ptr;
char* s;
int n;
float f;
struct addr_info addrinfo;
} any;
Le void* contenu dans la structure addr_info correspondra à l'adresse
mémoire allouée et size correspondra à la taille de la zone.
La fonction de comparaison et la fonction de hachage porteront uniquement sur l'adresse mémoire.
Commencer par écrire les fonctions de comparaison, d'affichage, et de hachage.
Note: la fonction de hachage peut être très simple (par exemple, l'adresse divisée par 4,
étant donné que les deux derniers bits d'un pointeur sont toujours nuls).
Ça va être un peu long (mais d'autant plus agréable, comme toujours)!
ATTENTION! Bien sûr il y a un piège. Comme le code de gestion des tables de hachage utilise malloc
et free, on va avoir un problème de réentrance (lorsqu'on entrera dans notre malloc, on va
faire appel aux fonctions des tables de hachage, qui vont elles-mêmes faire appel à malloc,
qui va lui-même faire appel aux fonctions des tables, etc). Suggestion: dans l'ensemble du code des
tables de hachage,
remplacer les appels aux fonctions malloc et free par un appel à des
pointeurs de fonctions statiques vers les vraies fonctions malloc et free, exactement
comme vous l'avez déjà fait dans le code de votre propre version de malloc.
Ecrire une fonction initialisation_allocateur qui sera appelée lors du chargement de la bibliothèque.
Pour indiquer que cette fonction doit être appelée au début du programme, il faut la déclarer comme suit :
void initialisation_allocateur(void) __attribute__ ((constructor));
Dans cette fonction, nous allons initialiser la table de hachage.
De même écrire la fonction fin_allocateur qui sera appelée à la fin du programme pour
afficher ses conclusions sur l'utilisation de la mémoire par le programme. Pour
cela il faut déclarer cette fonction avec l'attribut destructor :
void fin_allocateur(void) __attribute__((destructor));
À la sortie du programme, il faudra vérifier que toute la mémoire a bien été libérée... Attention il
faut réfléchir un peu pour mettre cela en place. N'oubliez pas vos neurones au vestiaire, notamment
vis-à-vis de cette petite blagueuse de fonction realloc! Allez, pour vous aider, voici un
programme de test sur lequel vous pourrez utiliser votre allocateur pour vérifier que vous ne faites pas
(trop) de bêtises:
Exercice 5 - Ce petit farceur de ls
Une fois que votre allocateur fonctionne sur le programme de test donné dans l'exercice
précédent, essayez-le avec gcc. Si ça fonctionne, criez de joie et essayez ensuite
avec la commande ls. Pleurez à chaudes larmes. Pour voir ce qui se passe, mettez un
affichage au tout début de votre fonction calloc et relancez. Il y a un dépassement de
pile dû au fait que dlsym peut avoir besoin de calloc, ce qui provoque
un problème de réentrance. Pour résoudre ce problème, nous allons utiliser une horrible ruse.
En effet, quand dlsym a besoin de calloc, c'est pour utiliser un petit tableau
(une trentaine d'octets). On peut donc tester grâce à une variable statique si l'on se trouve
ou non dans le premier appel à calloc. Si oui, on renvoie l'adresse d'un tableau
statique rempli de zéros. Si c'est le second appel, on recherche le vrai calloc
avec dlsym. Attention toutefois à ne pas libérer avec free l'adresse du
tableau statique.
Exercice 6 - Ce petit farceur de find
Une fois que votre allocateur fonctionne avec ls, essayez-le
avec la commande find. Pleurez encore. Pour voir ce qui ne va pas, lancez la commande
strace find. Vous verrez que le dernier appel système fait par le programme
find avant de quitter est close(2), ce qui signifie que le programme ferme
sa sortie d'erreur. Or, comme nos macros d'affichage tentent d'écrire sur la sortie d'erreur,
et que notre affichage final a lieu dans le destructeur qui est invoqué après la dernière
instruction du programme find, l'écriture échoue.
Une façon élégante de résoudre ce problème est d'écrire directement sur le terminal dans lequel
a été lancé le programme, car dans ce cas, il n'est pas possible de fermer le descripteur
de fichier et il n'est pas possible de rediriger ce qui a été écrit.
Pour cela, il faut ouvrir en écriture le fichier spécial /dev/tty avec la fonction
open. Faites-le dans le constructeur de la bibliothèque et affectez le descripteur
de fichier ainsi obtenu à la variable statique outstream utilisée par les macros
d'affichage. Testez et criez de joie.
Exercice 7 - Détection de débordement
Pour finir cet allocateur, nous allons implanter un petit système permettant de détecter les
débordements mémoires (lorsqu'on écrit au délà de la zone mémoire allouée).
Pour celà, à chaque fois que l'utilisateur demande une zone mémoire de taille t,
nous allons allouer une zone de taille t + 2*x :
Les deux bords (avant et arrière) sont initialisés à une valeur v tirée au
hasard (man random) :
Ainsi, à chaque demande d'allocation de t octets, nous allons :
- allouer t + 2*x octets
- tirer une valeur v au hasard
- initialiser les bords avec v
- Enfin, on fournit à l'utilisateur l'adresse correspondant à l'espace demandé:
memoire + x
Lors de la libération d'une zone mémoire, nous pouvons maintenant vérifier que les bords sont
restés inchangés. Pour cela on parcourt les deux bords et l'on vérifie qu'ils contiennent
bien la valeur v.
Implanter ce système de bords dans votre allocateur. Pour cela, vous devrez ajouter un champ
pour v au type struct addr_info. Attention: lors de la recherche d'une
adresse dans la table,
penser à intégrer le décalage dû au bord. La valeur x sera définie comme étant une
macro avec comme valeur 10.
© Université de Marne-la-Vallée