:: Enseignements :: Licence :: L2 :: 2009-2010 :: Système ::
[LOGO]

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 :

   x          t          x   
Les deux bords (avant et arrière) sont initialisés à une valeur v tirée au hasard (man random) :

vvvvvv       t       vvvvvvv
Ainsi, à chaque demande d'allocation de t octets, nous allons :

  1. allouer t + 2*x octets
  2. tirer une valeur v au hasard
  3. initialiser les bords avec v
  4. 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.