SkayaWiki

L2.2-03

JeromePetazzoni :: DerniersChangements :: DerniersCommentaires? :: ParametresUtilisateur :: http://www.enix.org/ :: Vous êtes ec2-52-14-224-197.us-east-2.compute.amazonaws.com

Locale, variables d'environnement et bibliothéque


On appelle locale l'ensemble des paramètres d'un utilisateur, qui dépendent de la langue et des conventions en vigueur "localement" - c'est-à-dire dans son pays ou dans sa région. C'est par le biais de ce mécanisme que l'on pourrait indiquer qu'on souhaite utiliser le français comme langue par défaut, que l'on souhaite que les nombres soient affichés sous la forme 1 234 567,57 au lieu de 1,234,567.57, que la date soit affichée comme "31 décembre 2005" au lieu de "December, 31st 2005", etc.

Dans un système POSIX (comme Linux, BSD...), on peut spécifier la locale qu'on souhaite utiliser par le biais de variables d'environnement. Pour voir le paramétrage actuel, et les différentes variables que l'on peut positionner, il suffit de taper locale. Pour voir toutes les locales paramétrées sur le système, il suffit de taper locale -a (avec un a comme all). Voici quelques variables (la liste complète est disponible, devinez où ? Dans la manpage, man locale!) :

- LANG : langue par défaut du système (à changer pour avoir les manpages en anglais!)
- LC_TIME : format de la date
- LC_NUMERIC : format d'affichage des chiffres
- LC_MONETARY : format d'affichage de sommes
- LC_ALL : si cette variable est positionnée, elle fixe la valeur de toutes les autres variables de locale (c'est celle qu'on va utiliser le plus souvent)

En utilisant la commande export, modifier la valeur des différentes variables de locale et constater l'impact de ces changements sur les commandes date, man, ls /toto, ...

Petite note culturelle : on entend parfois parler d'I18N et de L10N ; cela veut dire "internationalization" et "localization", les nombres 18 et 10 étant le nombre de lettres "manquantes". L10N est souvent employé dans des contextes de traduction ; I18N est encore plus générique - car par exemple, si on traduit une application en japonais, il ne suffit pas de changer la chaîne de caractère "Hello" en "Salut" ou "Holà", mais d'avoir les fontes nécessaires (kanji, kana, ...), le mécanisme d'affichage nécessaire, etc.

Souvent, les noms des "locales" se découpent en 3 parties : langage_VARIANTE.charset ; langage indique la langue utilisée, VARIANTE pourra changer pour différencer par exemple le français de France et le français du Québec (ou de Suisse, ou de Belgique...) ; quant à "charset", il indique comment traduire les accents et caractères spéciaux. Faites un coup de google sur utf8, latin-1, iso-8859-15, pour avoir une idée des problèmes sous-jacents... c'est payant !

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, alors charger la bibliothèque libmessage_POSIX.so. Ensuite charger la fonction message (man dlsym) dans cette bibliothèque et appeler cette fonction.

Traceur d'allocations mémoire


Le but de cette partie est le suivant : on veut garder une trace de chaque malloc, calloc, realloc et free effectué 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 utiliser un mécanisme déjà vu : une table de hachage !

Le "void*" contenu dans la table de hachage correspondera à l'adresse mémoire allouée. La fonction de comparaison et la fonction de hachage porteront sur cette adresse mémoire.

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/find ne l'est pas.

Préliminaires


Les choses sérieuses commencent. Comme dlopen alloue de la mémoire (avec malloc!), on ne va pas pouvoir l'utiliser, puisqu'on détourne malloc, précisément. 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. N.B.: il faudra définir la macro _GNU_SOURCE dans le code avant d'inclure dlfcn.h pour que ça marche (la macro RTLD_NEXT n'est pas définie sinon).

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.

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 suivantes:


/* Debut des lignes à mettre au début du fichier allocateur.c */
static char hexa[16] = { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'} ;

static char affiche_buffer[12];
#ifndef NMESSAGE
#define affiche_chaine(s) write(2,s,strlen(s))
#define affiche_hexa(v) {int i; for(i=28;i>=0;i-=4) write(2,hexa+(((int)(((int)v)>>i))&0xF),1) ; }
#define affiche_entier(v) {char affiche_buffer[12]; affiche_buffer[11]='\0'; int i=v; int l=10; if (i==0) write(2,"0",1); else { while(i>0) { affiche_buffer[l]='0'+i%10; i/=10; l--;} l++; write(2,affiche_buffer+l,11-l);} }
#else
#define affiche_chaine(s)
#define affiche_hexa(v)
#define affiche_entier(v)
#endif
/* Fin des lignes */

chaque #define doit être sur une seule ligne
La première marcro (affiche_chaine) affiche une chaîne de caractère sur la sortie erreur standard et la deuxième (affiche_hexa) affiche une valeur (pointeur par exemple) en hexadecimal sur la sortie erreur standard et enfin la troisième (affiche_entier) affiche une valeur entière. Enfin, pour désactiver l'affichage des messages d'erreurs, il suffit de déclarer une macro NMESSAGE, soit dans le fichier ( #define NMESSAGE avant les macros bien sûre) soit en ajoutant l'option -DNMESSAGE sur la ligne de compilation.

Le vif du sujet


Il faut maintenant écrire une bibliothèque (c'est-à-dire un programme sans main, en sorte!) 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 dlopen+dlsym. Indication : utiliser une variable static initialisée à NULL dans chaque fonction pour stocker le pointeur vers la "vraie" fonction. 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=mon_malloc.so programme-a-lancer - le LD_PRELOAD va indiquer au "linker dynamique" de commencer par chercher les fonctions "manquantes" dans mon_malloc.so avant de piocher dans les autres bibliothèques dynamiques du système.

De plus en plus rude


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 qui a été allouée, pour la suivre à la trace lors des libérations et réallocations.

Pour cela, 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 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 à vos pointeurs de fonctions vers les vraies fonctions malloc et free.

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:

extern void initialisation_allocateur(void) __attribute__ ((constructor));

Dans cette fonction, nous allons initialiser les pointeurs de fonctions vers les vraies fonctions d'allocation et initialiser la table de hachage.

De même écrire la fonction fin_allocateur qui sera appelée à la fin du programme. Pour cela il faut déclarer cette fonction avec l'attribut destructor:

extern 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 !

Le baptême du feu


Testez l'ensemble avec une application un peu plus conséquente (gimp, mozilla, openoffice...). Si ça marche, levez les deux bras en l'air et criez victoire d'un air convaincu.


Table de hachage encore plus générique


Afin de prendre mieux en compte le problème de réentrance décrit plus haut, on va modifier les tables de hachage ainsi que les listes chaînées afin de les rendre générique vis-à-vis des fonctions d'allocation et désallocation.

Modifier la structure classe_s afin qu'elle contienne deux pointeurs de fonctions supplémentaires:
- void *(*allocateur)(int ) Pointeur vers la fonction d'allocation.
- void (*desallocateur)(void *) Pointeur vers la fonction de désallocation.

Reprendre tous les appels à malloc et free dans le code des tables et listes et les remplacer par des appels à allocateur et desallocateur. Tester le programme dico avec ces nouvelles tables.

Il ne vous reste plus qu'à correctement initialiser les pointeurs de fonctions de la structure classe_s dans la fonction d'initialisation de la bibliothèque.


Gestion de la taille des allocations


On souhaite maintenant conserver dans la table de hachage non seulement le pointeur vers la zone mémoire allouée mais aussi
la taille de cette zone mémoire. Pour cela nous allons utiliser la structure suivante dans les tables
struct zone_s {
void *memoire;
int taille;
char bord;
}

Ne vous souciez pas de la valeur bord pour l'instant, c'est pour la question suivante.
Ecrire les fonctions de comparaison, de hachage et d'affichage nécessaires aux tables de hachage pour cette structure (les fonctions de hachage et comparaison utiliseront uniquement le champs memoire comme avant).
Modifier votre allocateur afin qu'il utilise la structure zone_s. Entre outre, la bibliothèque devra indiquer à la fin du programme le nombre total d'allocations mémoires effectuées au cours du programme, la quantité de mémoire allouée, le nombre de desallocation et la quantité de mémoire désallouée ainsi qu'afficher les informations sur les allocations encore dans la table.

Détection de débordement


Pour finir cet allocateur, nous allons implanté 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 cela, à chaque fois que l'utilisateur demande une zone mémoire de taille t, nous allons alloué 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):

vvvvvvv t vvvvvvv

Ainsi, à chaque demande d'allocation de t octets, nous allons :
- allouer t + 2*x octets
- tirer une valeur v au hasard
- Initialiser l'ensemble de la zone avec v (man memset)
- Construire une structure zone_s avec le champs mémoire initialisé sur la zone allouée, le champs taille initialisé à t et le champs bord initialisé avec la valeur 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 bord dans votre allocateur. 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 par défaut.

Pour finir, déclarer x comme étant une variable static globale. Dans la fonction d'initialisation de la bibliothèque initialiser x à partir de la valeur de la variable d'environnement ALLOCATEUR_BORD si celle-ci existe, à 10 sinon.
Il n'y a pas de commentaire sur cette page. [Afficher commentaires/formulaire]