• Aucun résultat trouvé

Fonctions d'encadrement personnalisées

Dans le document Programmation système en C sous (Page 179-184)

Notons que l'arrêt brutal du programme est dû à la double libération du dernier pointeur.

Cette erreur avait été détectée lors de l'appel de free( ), mais comme nous n'avons rien fait d'autre que d'afficher un message, la seconde tentative de libération a eu lieu normalement.

Il est aussi possible de demander aux routines d'allocation d'effectuer le même genre de surveillance simplement en définissant la variable d'environnement MALLOC_CHECK_. Dès que cette variable est définie, les routines d'allocation deviennent plus tolérantes, permettant les multiples libérations d'un même bloc ou les débordements d'un octet en haut ou en bas d'un bloc. Ensuite, en fonction de la valeur de MALLOC_CHECK_, le comportement varie lorsqu'une erreur est rencontrée :

• Si MALLOC_CHECK_ vaut 1, un message est inscrit sur la sortie d'erreur standard.

• Si MALLOC_CHECK_ vaut 2, le message est inscrit, puis le processus est arrêté avec un abort( ). Le fichier core créé permettra de retrouver l'endroit où l'erreur s'est produite.

• Pour toute autre valeur de MALLOC_CHECK_, l'erreur est silencieusement ignorée.

Autant dire que ce n'est pas une méthode raisonnable pour éliminer un dysfonctionnement (quoique cela assure une certaine protection pendant une démo !).

Le programme exemple_mcheck_2.c effectue les mêmes opérations que exemple_mcheck_1.c, mais il ne met pas en place de gestionnaire d'erreur. De plus, la table allouée contient des caractères et non plus des entiers, ce qui permet de rentrer dans le cadre de tolérance de MALLOC_CHECK_ pour les débordements d'un octet. Voici un exemple d'exécution de ce programme avec diverses configurations de la variable d'environnement :

$ unset MALLOC_CHECK_

$ ./exemple_mcheck_2 Allocation de la table On déborde vers le haut Libération de la table Allocation de la table On déborde vers le bas Libération de la table

Segmentation fault (core dumped)

$ export MALLOC CHECK =1

$ ./exemple_mcheck_2 Allocation de la table malloc: using debugging hooks On déborde vers le haut Libération de la table

free( ): invalid pointer 0x8049830!

Libération de la table

free( ): invalid pointer 0x80498501 Allocation de la table

Écriture normale Libération de la table

Et re-libération de la table ! free( ): invalid pointer 0x80498701

$ export MALLOC_CHECK_ =2

$ ./exemple_mcheck_2 Allocation de la table On déborde vers le haut Libération de la table Aborted (core dumped)

$ export MALLOC_CHECK_ =0

$ ./exemple_mcheck_2 Allocation de la table On déborde vers le haut Libération de la table Allocation de la table On déborde vers le bas Libération de la table Allocation de la table Écriture normale

Libération de la table

Et re-libération de la table !

$

La première exécution (MALLOC_CHECK_ non définie) échoue lors de la tentative de double libération du pointeur. La seconde (valeur 1) affiche les erreurs, mais les tolère. La troisième exécution (valeur 2) affiche les erreurs et s'arrête dès qu'une incohérence est rencontrée. Enfin, la dernière exécution (valeur quelconque, 0 en l'occurrence) autorise toutes les erreurs sans afficher de message.

Fonctions d'encadrement personnalisées

Nous avons indiqué que les fonctionnalités de surveillance, comme mcheck( ), utilisent des points d'entrée dans les routines d'allocation et de libération pour insérer leur code.

Mais nous pouvons également utiliser ces points d'entrée pour y glisser nos propres fonctions de super-vision. Ce genre de procédé de débogage est relativement pointu et n'est généralement nécessaire que pour des logiciels vraiment conséquents, où un processus particulier est, par exemple, chargé de surveiller le déroulement de ses confrères. L'insertion d'une routine de débogage se fait en utilisant les variables globales suivantes, déclarées dans <malloc.h> :

Variable Utilisation

_malloc_hook Pointeur sur une fonction du type

void * fonction (size_t taille, void * appelant) Cette routine sera appelée à la place de malloc( ) ; elle reçoit en argument la taille de bloc à allouer et un pointeur contenant l'adresse de retour, ce qui permet de retrouver l'emplacement de l'appel fautif si une erreur est détectée. Cette fonction doit renvoyer un pointeur sur la zone de mémoire nouvellement allouée. Nous montrerons plus bas comment invoquer l'ancienne routine d'allocation pour obtenir le bloc mémoire désiré.

Variable Utilisation

_realloc_hook Pointeur sur une routine de type

void *fonction(void *ancien,size_t taille, void *appel) Cette fonction sera appelée lorsqu'on invoquera realloc( ) et devra s'occuper de redimensionner l'ancien bloc avec la nouvelle taille désirée.

L'emplacement où on a fait appel à realloc( ) est transmis en troisième argument pour retrouver une éventuelle erreur.

_free_hook Pointeur sur une routine de type

void fonction (void * pointeur, void * appelant)

chargée de libérer le bloc de mémoire correspondant au pointeur transmis.

Nous avons parlé de fonctions d'encadrement personnalisées, et non de fonctions d'allocation et de libération personnalisées. En effet, bien qu'il soit possible d'écrire nos propres routines de gestion complète de la mémoire, ce travail serait très difficile, et nous allons nous contenter d'insérer du code servant de tremplin pour l'appel des véritables routines malloc( ), realloc( ) et free( ). Dans notre exemple, nous nous contenterons d'afficher sur la sortie d'erreur standard les appels effectués. Les routines d'encadrement pourraient être bien plus subtiles, en vérifiant l'intégrité des blocs mémoire par exemple, comme nous le verrons plus loin.

Pour pouvoir faire appel aux routines originales malloc( ), realloc( ) ou free( ), il est nécessaire de stocker dans des variables globales les valeurs initiales des points d'entrée. Lorsque nous désirerons les invoquer, il suffira de restituer les valeurs originales des points d'entrée et de faire un appel normal à la fonction concernée. Comme nous ne connaissons pas les inter-dépendances entre les routines de la bibliothèque, il faudra à chaque fois sauver, modifier et restituer l'ensemble des trois points d'entrée.

On notera aussi qu'au retour d'une des véritables fonctions d'allocation, il nous faudra sauver à nouveau les trois points d'entrée et réinstaller nos routines. En effet, une routine comme malloc( ) peut pointer à l'origine sur une fonction d'initialisation qui, après son exécution, modifiera son propre point d'entrée pour accéder directement au code d'allocation durant les invocations ultérieures. Une routine peut donc modifier son propre point d'entrée ou celui des autres fonctions, et on sauvera de nouveau à chaque fois les trois pointeurs. Voyons donc un exemple de fonctions d'encadrement.

exemple_hook.c :

#include <stdio.h>

#include <stdlib.h>

#include <malloc.h>

static void * pointeur_malloc = NULL;

static void * pointeur_realloc = NULL;

static void * pointeur_free = NULL;

static void * mon_malloc (size_t taille, void * appel);

static void * mon_realloc (void * ancien, size_t taille, void appel);

static void mon_free (void * pointeur, void * appel);

int main (void)

{

char * bloc:

/* Installation originale */

#ifndef NDEBUG

pointeur_malloc = _malloc_hook;

pointeur_realloc = _realloc_hook;

pointeur_free = _free_hook;

_malloc_hook = mon malloc;

_realloc_hook = mon_realloc;

_free_hook = mon_free;

#endif

/* et maintenant quelques appels... */

bloc = malloc (128);

bloc = realloc (bloc, 256);

bloc = realloc (bloc, 16);

free (bloc);

bloc = calloc (256, 4);

free (bloc);

return (0);

}

static void *

mon_malloc (size_t taille, void * appel) {

void * retour;

/* restitution des pointeurs et appel de l'ancienne routine */

_malloc_hook = pointeur_malloc;

_realloc_hook = pointeur_realloc;

_free_hook = pointeur_free;

retour = malloc (taille);

/* Écriture d'un message sur stderr */

fprintf (stderr, "%p : malloc (%u) -> %p \n", appel, taille, retour);

/* on réinstalle nos routines */

pointeur_malloc = _malloc_hook;

pointeur_realloc = _realloc_hook;

pointeur_free = _free_hook;

_malloc_hook = mon_malloc;

_realloc_hook = mon_realloc;

_free_hook = mon free;

return (retour);

}

static void *

mon_realloc (void * ancien, size_t taille, void * appel) {

void * retour;

/* restitution des pointeurs et appel de l'ancienne routine */

_malloc_hook = pointeur_malloc;

_realloc_hook = pointeur_realloc;

_free_hook = pointeur_free;

retour = realloc (ancien, taille);

/* Écriture d'un message sur stderr */

fprintf (stderr, "%p : realloc (%p, %u) -> %p \n", appel, ancien, taille, retour);

/* on réinstalle nos routines */

pointeur_malloc = _malloc_hook;

pointeur_realloc = _realloc_hook;

pointeur_free = _free_hook;

_malloc_hook = mon_malloc;

_realloc_hook = mon_realloc;

_free_hook = mon_free;

return (retour);

}

static void

mon_free (void * pointeur, void * appel) {

/* restitution des pointeurs et appel de l'ancienne routine */

_malloc_hook = pointeur_malloc;

_realloc_hook = pointeur_realloc;

_free_hook = pointeur_free;

free (pointeur);

/* Écriture d'un message sur stderr */

fprintf (stderr,"%p : free (%p)\n", appel, pointeur);

/* on réinstalle nos routines */

pointeur_malloc = _malloc_hook;

pointeur_realloc = _realloc_hook;

pointeur_free = _free_hook;

_malloc_hook = mon_malloc;

_realloc_hook = mon_realloc;

_free_hook = mon_free;

}

Le fait d'utiliser l'argument void * appel dans les routines est en fait une astuce permettant de récupérer l'adresse de retour directement dans la pile. En fait, les variables malloc_hook, realloc_hook et free_hook sont conçues pour stocker des pointeurs sur des fonctions ne comportant pas ce dernier argument. Il ne faut donc pas s'étonner des avertissements fournis par le compilateur. On peut les ignorer sans danger.

On remarquera que l'encadrement par #ifndef NDEBUG #ifendif de l'initialisation de nos routines permet d'éliminer ce code de débogage lors de la compilation pour la version de distribution du logiciel. Voici à présent un exemple d'exécution :

$ make

cc -Wall -g exemple_hook.c -o exemple_hook exemple_hook.c: In function 'main':

exemple_hook.c:24: warning: assignment from incompatible pointer type exemple_hook.c:25: warning: assignment from incompatible pointer type exemple_hook.c:26: warning: assignment from incompatible pointer type exemple_hook.c: In function 'mon_malloc':

exemple_hook.c:56: warning: assignment from incompatible pointer type exemple_hook.c:57: warning: assignment from incompatible pointer type exemple_hook.c:58: warning: assignment from incompatible pointer type

exemple_hook.c: In function 'mon_realloc':

exemple_hook.c:79: warning: assignment from incompatible pointer type exemple_hook.c:80: warning: assignment from incompatible pointer type exemple_hook.c:81: warning: assignment from incompatible pointer type exemple_hook.c: In function 'mon_free':

exemple_hook.c:99: warning: assignment from incompatible pointer type exemple_hook.c:100: warning: assignment from incompatible pointer type exemple_hook.c:101: warning: assignment from incompatible pointer type

$ exemple_hook

0x804859c : malloc (128) -> 0x8049948

0x80485b2 : realloc (0x8049948, 256) -> 0x8049948 0x80485c5 : realloc (0x8049948, 16) -> 0x8049948 0x80485d6 : free (0x8049948)

0x80485e5 : malloc (1024) -> 0x8049948 0x80485f6 : free (0x8049948)

$

Nous voyons bien tous nos appels aux trois routines de surveillance et aussi, que calloc(

) est construit en invoquant malloc( ) , ce qui est rassurant car il n'existe pas de point d'entrée _calloc_hook. Les adresses fournies lors de l'invocation peuvent paraître particulièrement obscures, mais on peut aisément utiliser gdb pour retrouver la position de l'appel dans le programme. Recherchons par exemple où se trouve le second realloc(

) :

$ gdb exemple_hook

GNU gdb 4.17.0.11 with Linux support [...]

(gdb) list *0x80485c5

0x80485c5 is in main (exemple_hook.c:32).

27 #endif 28

29 /* et maintenant quelques appels... */

30 bloc = malloc (128);

31 bloc = realloc (bloc, 256);

32 bloc = realloc (bloc, 16);

33 free (bloc);

34 bloc = calloc (256, 4);

35 free (bloc); 36 (gdb) quit

$

En entrant « list *» suivi de l'adresse recherchée, gdb nous indique bien qu'il s'agit de la ligne 32 du fichier exemple_hook.c, dans la fonction main( ).

Mais que d'énergie déployée pour obtenir grosso modo le même résultat qu'en invoquant mtrace( ) en début de programme et en définissant la variable d'environnement MALLOC_TRACE ! En fait, nous pouvons utiliser ces points d'entrée dans les routines de gestion mémoire pour effectuer des vérifications d'intégrité beaucoup plus poussées. On peut être confronté à des débordements de buffer d'un seul octet, par exemple à cause d'une mauvaise borne supérieure d'un intervalle, d'une utilisation de l'opérateur <= au lieu de <, ou tout simplement en oubliant de compter le caractère nul à la fin d'une chaîne. Ces bogues sont difficiles à détecter car, du fait de l'alignement des blocs sur des adresses multiples de 8 ou de

16 octets suivant les architectures, le débordement d'un seul caractère peut parfaitement passer inaperçu pendant longtemps.

Nous allons considérer ici qu'un long int occupe 4 octets, ce qui est le cas sur les architectures x86, mais on peut reprendre le même raisonnement en utilisant simplement sizeof (long int) pour rester portable. Nous pouvons encadrer toutes les zones de mémoire allouées par deux blocs de 8 octets supplémentaires. Le bloc inférieur contiendra la longueur de la mémoire allouée (sur 4 octets) et une valeur constante sur les 4 octets suivants. Le bloc supérieur comprendra deux fois cette valeur constante. La zone de mémoire renvoyée à l'appelant sera comprise entre les deux blocs de surveillance.

Imaginons que le programme demande une allocation de 256 octets, nous en avons en réalité 256+16, soit 272 octets organisés comme sur la figure 13-2.

Lorsque nous allouerons un bloc, nous remplirons les données de surveillance correctement. Ensuite, quand nous recevrons un pointeur sur un bloc à redimensionner ou à libérer, nous vérifierons que les données sont toujours en place. Si ce n'est pas le cas, le processus est arrêté avec abort( ). Avant de libérer un bloc, nous écraserons toutes les données qu'il contient pour nous assurer qu'il n'est plus valide. Le programme exemple_hook_2.c va faire déborder une copie de chaîne en oubliant volontairement le caractère nul.

exemple_hook_2.c : #include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <malloc.h>

static void * pointeur_malloc = NULL;

static void * pointeur_realloc = NULL;

static void * pointeur_free = NULL;

static void * mon_malloc (size_t taille);

static void * mon_realloc (void * ancien, size_t taille, void * appel);

static void mon_free (void * pointeur, void * appel);

static int verifie_pointeur (void * pointeur);

#define RESTITUTION POINTEURS( ) _malloc book = pointeur_malloc;

_realloc_hook = pointeur_realloc; \ _free_hook = pointeur_free;

#define SAUVEGARDE_POINTEURS( ) pointeur_malloc = _malloc_hook;

pointeur_realloc = _realloc_hook; \ pointeur_free = _free_hook;

Figure 13.2 Encadrement du

bloc alloué 256 0x12345678 0x12345678 0x1234567E

4 octets 4 octets 256 octets 4 octets 4 octets

Longueur du bloc renvoyé

Valeur magique

Bloc de mémoire réellement

renvoyé

Valeur

magique Valeur magique

#define INSTALLATION_ROUTINES( )

_malloc_hook = mon_malloc;

_realloc_hook = mon_realloc; \ _free_hook = mon_free

int main (void) {

char * bloc = NULL;

char * chaine = "chaîne à copier";

/* Installation originale */

#ifndef NDEBUG

SAUVEGARDE_POINTEURS( );

INSTALLATION_ROUTINES( );

#endif

/* Une copie avec oubli du caractère final */

bloc = malloc (strlen (chaine));

if (bloc != NULL)

strcpy (bloc, chaine);

free (bloc);

return (0);

}

#define VALEUR_MAGIQUE Ox12345678L static void *

mon_malloc (size_t taille) {

void * bloc;

RESTITUTION_POINTEURS( );

bloc = malloc (taille + 4 * sizeof(long));

SAUVEGARDE_POINTEURS( );

INSTALLATION_ROUTINES( );

if (bloc == NULL) return (NULL);

/* et remplissage des données supplémentaires */

* (long *) bloc = taille;

* (long *) (bloc + sizeof (long)) = VALEUR_MAGIQUE;

* (long *) (bloc + taille + 2 * sizeof (long)) = VALEUR_MAGIQUE;

* (long *) (bloc + taille + 3 * sizeof (long)) = VALEUR_MAGIQUE;

/* on renvoie un pointeur sur le bloc réservé à l'appelant */

return (bloc + 2 * sizeof (long));

}

static void *

mon_realloc (void * ancien, size_t taille, void * appel) {

void * bloc;

if (! verifie_pointeur (ancien) {

fprintf (stderr, "%p : realloc avec mauvais bloc\n", appel);

abort( );

}

RESTITUTION_POINTEURS( ) ; if (ancien != NULL)

bloc = realloc (ancien - 2 * sizeof (long), taille + 4 * sizeof(long));

else

bloc = malloc (taille + 4 * sizeof (long));

SAUVEGARDE_POINTEURS( ) ; INSTALLATION_ROUTINES( ) ; if (bloc == NULL)

return (bloc);

/* et remplissage des données supplémentaires *1 * (long *) bloc = taille;

* (long *) (bloc + sizeof (long)) = VALEUR_MAGIQUE;

* (long *) (bloc + taille + 2 * sizeof (long)) = VALEUR_MAGIQUE;

* (long *) (bloc + taille + 3 * sizeof (long)) = VALEUR_MAGIQUE;

/* on renvoie un pointeur sur le bloc réservé à l'appelant */

return (bloc + 2 * sizeof (long));

}

static void

mon_free (void * pointeur, void * appel) {

long taille;

long i;

if (! verifie_pointeur (pointeur)) {

fprintf (stderr, "%p : free avec mauvais bloc\n", appel);

abort( );

}

if (pointeur == NULL) return;

RESTITUTION_POINTEURS( );

/* écrabouillons les données ! */

taille = (* (long *) (pointeur - 2 * sizeof(long)));

for (i = 0; i < taille + 4 * sizeof (long); i++) * (char *) (pointeur - 2 * sizeof (long) + i) = 0;

/* et libérons le pointeur */

free (pointeur - 2 * sizeof (long));

SAUVEGARDE_POINTEURS( ) ; INSTALLATION_ROUTINES( ); }

static int

verifie_pointeur (void * pointeur) {

long taille;

if (pointeur = NULL) return (1);

if (* (long *) (pointeur - sizeof (long)) != VALEUR_MAGIQUE) return (0);

taille = * (long *) (pointeur - 2 * sizeof (long));

if (* (long *) (pointeur + taille) != VALEUR_MAGIQUE) return (0);

if (* (long *) (pointeur + taille + sizeof (long)) != VALEUR_MAGIQUE) return (0);

return (1);

}

Nous avons défini des macros pour remplacer les trois lignes de copie de pointeurs pour raccourcir et simplifier le fichier source. Voici un exemple d'exécution avec détection du débordement de la chaîne :

$ ./exemple_hook_2

0x804867b : free avec mauvais bloc Aborted (core dumped)

$ rm core

$

Bien entendu, ce genre de routine ne permet pas de détecter tous les défauts de gestion mémoire dans un programme, mais on peut l'associer avec les autres méthodes, comme retrace( ), pour vérifier que l'application est aussi robuste qu'elle en a l'air extérieurement.

Conclusion

Ce chapitre a permis de mettre en place les principes fondamentaux de la gestion de la mémoire d'un processus. Il s'agit de notions élémentaires et de routines présentes dans l'essentiel des applications courantes. Les méthodes de débogage ou de paramétrage présentées ici sont particulièrement précieuses, mais elles ne sont malheureusement pas portables en dehors d'un environnement de programmation Gnu. Le prochain chapitre va nous permettre d'aborder des notions plus pointues sur la manipulation de l'espace mémoire d'un processus.

Les fonctions générales d'allocation mémoire sont décrites dans [KERNIGHAN 1994] Le langage C. Pour obtenir des précisions sur les rapports entre les fonctions de bibliothèque comme malloc( ) et les appels-système comme brk( ) ou mmap( ), on consultera avec profit les sources de la bibliothèque GlibC. On notera également qu'une discussion concernant les risques induits par les allocations dynamiques et que des idées pour dépister les bogues sont présentées dans [MCCONNELL 1994] Programmation professionnelle.

14

Gestion avancée de

Dans le document Programmation système en C sous (Page 179-184)