Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence

Allocation de mémoire sécurisée
en C et C++

7 Avril 2003

Par Haypo

Sommaire :

Introduction :

Le but de cet article est d'écrire un allocateur de mémoire sécurisé. Par "sécurisé", j'entend qu'il devra être capable de :

  1. Générer une erreur en cas d'échec d'allocation de mémoire (plus assez de mémoire disponnible)
  2. Lister la mémoire (les pointeurs) non libérée (libérés)
  3. Ajoute des informations à chaque pointeur alloué : nom du fichier et ligne du fichier où le pointeur a été alloué, taille du pointeur, signature (pour vérifier que le pointeur soit valide), etc.
  4. Vérifier les écritures en dehors de la zone mémoire allouée (avant : "underflow", et après : "overflow")
  5. Donner la possibilité d'activer ou désactiver les différentes fonctions (à la compilation)
  6. Tout cela doit pouvoir s'appliquer sans modifier le code source existant

... Facile ! Ne vous inquiétez pas, je vais vous guider pas à pas.

Cet article est basé sur une implémentation en langage C, avec une extension pour le langage C++. Je pense qu'on peut adapter le principe à d'autres langages, mais il faudra modifier le code source.

ATTENTION: Le but de nos modifications ne visent non pas à rendre un programme plus sûr et inviolable, mais à corriger des erreurs dans le code source d'un programme.

Vous vous en douterez sûrement, ces modifications ont un coût sur les performances. C'est pour cela que j'ai rajouté le point (6) dans notre cahier des charges : il faut pouvoir désactiver une ou plusieurs (toutes) options. Je pense que le point (1) devrait toujours être actif : écrire dans un pointeur NULL provoque une erreur de segmentation, quelque soit le système d'exploitation !!!

NOTE: Rien ne sert de s'amuser à copier/coller le code source, un exemple complet est fourni à la fin de cette page.

Allocation de mémoire en C/C++ :

Le langage C nous offre plusieurs fonctions d'allocation de mémoire :

  • malloc (n) : alloue n octets de mémoire
  • calloc (n, taille) : alloue n*taille octets de mémoire
  • realloc (ptr, n) : redimensionne le pointeur ptr pour qu'il atteingne la taille de n octets (qui peut être inférieur ou supérieur à l'ancienne taille de ptr)
  • free (ptr) : libère la mémoire qui était allouée à l'adresse ptr

Et il existe encore des fonctions dérivées comme strdup qui est une combinaison de strlen (lit la longueur d'une chaîne de caractère), malloc (allocation de mémoire) et strcpy (copie les données d'une chaîne de caractères dans une autre).

Le langage C++ nous offres quatres opérateurs (et non plus fonctions) :

  • new objet : crée un objet
  • new objet[n] : crée un tableau de n objet(s)
  • delete ptr : efface un objet
  • delete []ptr : efface un tableau d'objets

En plus d'allouer la mémoire, il va également appeler le "constructeur" de chaque objet après l'allocation. Ceci est gêré automatiquement par l'opérateur new.

Prototype des fonctions d'allocations C :

On va commencer par redefinir les fonctions d'allocation de mémoire en C. Pour cela, il faut commencer par réécrire les fonctions en se basant sur le prototype standard du C :

void *malloc(size_t size);
void *realloc(void *block, size_t size);
void *calloc(size_t nitems, size_t size);
void free(void *block);

Pour éviter les conflits de noms, on va les appeler : MallocSecurise, ReallocSecurise, CallocSecurise et FreeSecurise. Il faut maintenant rajouter automatiquement le nom du fichier et la ligne du fichier où le pointeur a été alloué. Pour cela, le langage C nous offre deux macros : __FILE__ (nom : const char *) et __LINE__ (ligne : unsigned int). Le problème est de les passer automatiquement en paramètre sans modifier le code source existant ... Heureusement, le langage C nous laisse la possibilité d'écrire des macros (#define). On pourra donc utililiser :

// Prototypes
void *malloc(size_t size, const char* nomfich, unsigned int numligne);
void *realloc(void *block, size_t size, const char* nomfich, unsigned int numligne);
void *calloc(size_t nitems, size_t size, const char* nomfich, unsigned int numligne);
void free(void *block, const char* nomfich, unsigned int numligne);

// Macros
#define malloc(size) MallocSecurise(size, __FILE__, __LINE__)
#define realloc(ptr,new_size) ReallocSecurise(ptr, new_size, __FILE__, __LINE__)
#define calloc(n,size) CallocSecurise(n, size, __FILE__, __LINE__)
#define free(ptr) FreeSecurise(ptr, __FILE__, __LINE__)

Prototype des fonctions d'allocations C++ :

Pour le langage C++, on va redéfinir :

void* operator new (size_t size) throw (std::bad_alloc);
void* operator new[] (size_t size) throw (std::bad_alloc);
void operator delete (void *) throw ();
void operator delete[] (void *ptr) throw ();

Petit problème : on ne modifie plus des fonctions, mais des opérateurs. De plus, ces opérateurs sont spéciaux car new doit appeler le créateur de l'objet après l'avoir alloué. Pas question donc de renommer ces opérateurs ! Après avoir fait des tests, j'ai compris qu'on pouvait changer le nombre de paramètres de l'opérateur new, mais pas de l'opérateur delete ... Cela rend l'ajout automatique des paramètres encore un peu plus difficile. Il faut alors passer par une variable globale pour l'opérateur delete. Pour l'opérateur new, on va reprendre Ce qui va nous donner :

// Variables globales
extern const char *delete_FILE;
extern unsigned long delete_LINE;
// Opérateurs
void* operator new (size_t size, const char* nomfich, unsigned int numligne) throw (std::bad_alloc);
void* operator new[] (size_t size, const char* nomfich, unsigned int numligne) throw (std::bad_alloc);
void operator delete (void *) throw ();
void operator delete[] (void *ptr) throw ();

// Macro
#define new new (__FILE__, __LINE__)
#define delete delete_FILE=__FILE__, delete_LINE=__LINE__, delete

La macro "#define new new (__FILE__, __LINE__)" est un peu bizzare, "new nombre;" devient "new (__FILE__, __LINE__) nombre;". C'est spécial, mais le principal n'est pas que ça marche ?

Utiliser des virgules pour "#define delete ..." n'est pas très beau, mais ça marche ! Exemple de code qui poserait problème avec des points virgules :

for (unsigned int i=0; i<nbr_element; i++) delete liste[i];

Ce qui donne :

a) for (unsigned int i=0; i<nbr_element; i++) delete_FILE=__FILE__, delete_LINE=__LINE__, delete liste[i];
b) for (unsigned int i=0; i<nbr_element; i++) delete_FILE=__FILE__; delete_LINE=__LINE__; delete liste[i];

Vous ne voyez vraiment pas l'erreur dans b) ? Je vais vous aider : le compilateur bloque avec "symbole 'i' non défini" ... Note : le code a) est tout à fait valide !

Lister les pointeurs et structures des informations

L'algorithme le plus adapté pour lister des pointeurs alloués tout au long de l'exécution du programme est la liste doublement chaînée. J'ai testé avec une liste simplement chaînée, mais il fallait parcourir toute la liste pour supprimer un élément (pour corriger l'élément précédent) : dès qu'on dépassait 1000 éléments, les performances chutaient rapidement ...

Pour les informations ajoutées à chaque pointeur, je vous propose :

// Informations écrites avant les données du pointeur
typedef struct
{
  unsigned long nbr_magique; // Nombre magique (signature)
  bool est_alloue; // Pointeur alloué ou libéré ?
  size_t taille; // Taille allouée (sans informations de débogage)
  const char* nomfich; // Nom du fichier source
  ulong numligne; // Ligne du fichier
  struct PtrChaineMalloc *ptr_liste; // Pointeur dans la liste
  unsigned long somme_info; // Somme des informations (checksum)
} InfoMalloc;

Le membre "somme_info" est la somme simple (sur, au moins, 32 bits) des autres membres. Le membre "ptr_liste" est un pointeur vers l'élément de la liste des pointeurs alloués. Je ne pense pas que les autres membres ont besoin d'explications supplémentaires. Pour les pointeurs, je vous propose cette structure :

// Un pointeur chaîné
struct PtrChaineMalloc
{
  struct PtrChaineMalloc *precedent; // Elément suivant
  struct PtrChaineMalloc *suivant; // Elément suivant
  ulong nbr_magique; // Nombre magique (signature)
  InfoMalloc *ptr_info; // Pointeur à libérer
};

On retrouve les membres "precedent" et "suivant" du même type que la structure, typiques des listes doublement chaînées. Le membre "nbr_magique" permet de vérifier que le pointeur est bien un pointeur "valide".

Pour la structure de la liste elle-même, j'utilise :

// Le conteneur Malloc
struct
{
  struct PtrChaineMalloc *debut; // Pointeur vers le premier élément
  struct PtrChaineMalloc *fin; // Pointeur vers le dernier élément
} ConteneurMalloc = {NULL,NULL};

La liste est initialisée à {debut=NULL, fin=NULL} automatiquement !

Nous aurons besoin de deux fonctions :

  • Ajout d'un élément dans la liste
  • Suppression d'un élément de la liste

Vérification des écritures en dehors de la zone mémoire allouée

Vocabulaire :

  • Une erreur d'underflow (mot anglais) est une écriture en dehors d'un tampon, avant la mémoire allouée
  • Une erreur d'overflow (mot anglais) est une écriture en dehors d'un tampon, après la mémoire allouée

Ces deux erreurs sont très dangeureuse, car dans la plupart des ordinateurs le code source et les données sont placées dans le même espace mémoire. On peut donc modifier le programme en écrivant des instructions invalides. Dans le meilleur des cas, la mémoire n'est pas utilisée, et le programme continue comme si de ne rien n'était. Mais dans la plupard des cas, le programme réagit très bizzarement (plantages divers), ou alors une erreur de segmentation (SIGSEGV) est générée.

Il existe une méthode très simple à mettre en oeuvre : on alloue quelques octets en plus, on initialise la mémoire ajoutée en plus, enfin on vérifie que cette zone n'a pas été modifiée. Pour l'initialisation, le mieux est d'écrire une chaîne aléatoire, mais je préfère écrire une chaîne simple (on part d'une valeur, puis on incrémente à chaque itération). La taille de cette mémoire ajoutée ne doit pas être trop importante car chaque pointeur prendra cette taille en plus (ajoute 1024 octets sur 1024 pointeurs consomme "inutilement" 1 Mo ...). J'ai choisi 16 octets, mais 4 octets pourrait suffire ! Je rappelle que le but de nos modifications ne visent pas à rendre un programme inviolable, mais corriger des erreurs ...

Pour les erreurs d'underflow, la signature de nos informations et la "checksum" (somme de vérification) vont s'occuper de les détecter.

Explications de l'implémentation en C/C++ :

Je ne vais pas détailler tout le code, mais je vais vous présenter les deux fonctions principales : malloc et free.

Notre fonction malloc :

// Taille du tampon anti-overflow (en octets)
#define TAILLE_TAMPON_OVERFLOW 16

// Nombres magiques (32 bits) pour valider un pointeur
#define MALLOC_NBR_MAGIQUE 0x1A71A25C

// Allocation de mémoire sécurisée (malloc)
void* MallocSecurise (size_t size, const char *nomfich, unsigned long numligne)
{
  // Tente d'allouer le tampon
  void* ptr;
  size_t taille_allouee;
  InfoMalloc *info;
  char *overflow;
  unsigned long i;
  unsigned char rand = 0xA0;

  // Taille nulle : Erreur!
  if (size == 0) ERREUR ("Taille du pointeur à allouer nulle !");

  // Ajoute la taille des informations sur le pointeur
  taille_allouee = size +sizeof(InfoMalloc) +TAILLE_TAMPON_OVERFLOW;

  // Alloue la mémoire (appelle le malloc du langage C)
  ptr = malloc(taille_allouee);

  // Pointeur NULL : erreur!
  if (ptr == NULL)
    ERREUR ("Allocation de mémoire impossible : pas assez de mémoire libre !");

  // Ajoute le pointeur au conteneur
  info = (InfoMalloc *)ptr;
  CONTENEUR_MALLOC_AJOUTE (info);

  // Ecrit les informations sur le pointeur
  info -> nbr_magique = MALLOC_NBR_MAGIQUE;
  info -> est_alloue = true;
  info -> taille = taille_allouee;
  info -> nom_fich = nomfich;
  info -> num_ligne = numligne;
  info -> somme_info = MallocCalculSomme (info);

  // Prépare le tampon anti-overflow
  overflow = (char *)info +taille_allouee;
  for (i=0; i<TAILLE_TAMPON_OVERFLOW; i++) *ptr++ = rand++;

  // Renvoie le pointeur vers l'espace alloué vierge
  return (info+1);
}

// Calcule la somme de contrôle des informations d'un pointeur
ulong MallocCalculeSomme (const InfoMalloc *info)
{
  // Calcule la somme des données d'un pointeur
  return
   ((ulong)(info -> nbr_magique)
   +(ulong)(info -> est_alloue)
   +(ulong)(info -> taille)
   +(ulong)(info -> est_local)
   +(ulong)(info -> nom_fich)
   +(ulong)(info -> num_ligne));
}

Notre fonction free :

// Fonction 'free' sécurisé
void FreeSecurise (const void *ptr,
                     const char* nomfich, const ulong numligne)
{
  infoMalloc *info;
  ulong somme;

  // Pointeur NULL : La norme demande de ne rien faire, on va pas se gêner!
  if (ptr == NULL) return;

  // Lit les informations du pointeur
  info = (InfoMalloc *)ptr-1;

  //--- Vérifie les informations ---
  if (info -> nbr_magique != MALLOC_NBR_MAGIQUE)
    ERREUR ("Pointeur invalide !");

  // Calcule la somme des données
  if (info -> somme_info != MallocCalculeSomme (info))
    ERREUR ("Somme de vérification incorrect (%s:%u) !",
            info -> nom_fich, info -> num_ligne);

  // Vérifie qu'on est pas écrit en dehors du tampon
  if (!Malloc_TamponOverflowIntact(info))
    ERREUR ("Erreur d'overflow (%s:%u) !",
            info -> nom_fich, info -> num_ligne);

  // Sort le pointeur du conteneur en vérifiant les
  // informations sur le pointeur
  CONTENEUR_MALLOC_EFFACE (info -> ptr_liste);

  // Le pointeur n'est plus alloué ! (evite de libérer deux fois le même
  // pointeur) Même si la mémoire est 'libérée', elle n'est pas effacée
  // avec un 'free'.
  info -> est_alloue = false;

  // Enfin, libère la mémoire
  free (info);
}

Notes :

  • Le standard demande à ce que malloc(0) renvoie NULL, mais moi je n'aime pas NULL, alors je génère une erreur !
  • ERREUR (format, ...) est une fonction affichant un message d'erreur (ayant la forme de printf) puis quittant le programme.
  • CONTENEUR_MALLOC_AJOUTE (info) est une fonction qui ajoute un pointeur dans la liste des pointeurs.
  • CONTENEUR_MALLOC_EFFACE (info) est une fonction qui efface un pointeur de la liste des pointeurs.

Bien sûr, cet exemple est (volontairement) simplifié. Il est possible de mieux écrire le code, en particulier placer certaines parties dans des fonctions.

Les autres fonctions se résument à :

  • realloc : on utilise la fonction realloc du C, puis on retrouve le pointeur dans la liste des pointeurs, et on corrige son adresse.
  • calloc : ça se résume à appler notre malloc, puis faire un memset.
  • strdup : la bonne combinaison de strlen, (notre) malloc et strcpy.
  • new et new[] : appelle simplement notre malloc.
  • delete et delete[] : appelle simplement notre free.

Code complet à télécharger :

AVERTISSEMENT: Le code source proposé est sous licence GPL, je vous invite à la lire avant de réutiliser ce code !

Télécharger le code source sous licence GPL :

Je vous conseille également de télécharger le code source de ma calculatrice, car vous y trouverez la dernière version de ma librairie HAlloc (dans le répertoire "include" du code source). D'ailleurs le code donné plus haut est une version (très) simplifiée, même si elle est utilisable.

Revenir sur la page de Haypo

Responsables bénévoles de la rubrique Accueil : Nicolas Vallée (gorgonite) et Guillaume Rossolini (Yogui) - Contacter par EMail :
Vos questions techniques : forum d'entraide Accueil - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Copyright © 2000-2008 www.developpez.com - Legal informations.