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