Télécharger: s_malloc.c

Téléchargement de: s_malloc.c Débugguer les mallocs!

malloc vs Malloc

Attention à bien faire la différence entre malloc() en minuscules et Malloc() avec une majuscule. La première version est une fonction du C ANSI alors que la seconde est un appel système au GEMDOS spécifique à l'ATARI ST.

Le nombre de zones pouvant être allouées simultanément par le GEMDOS avec Malloc() est limité (à quelques centaines) et il est donc préférable d'utiliser la fonction malloc() du C, ce qui a de plus, l'avantage de la portabilité.

En fait, sur ATARI, malloc() fait appel à Malloc(), mais en utilisant une petite d'astuce. Chaque appel à Malloc() réserve un gros tampon de plusieurs dizaines, voire centaines de Kilo-octets (cela dépend de votre compilateur) et la bibliothèque du C qui les gère s'occupe de ranger dans ces grandes zones, tous vos petits malloc(), même s'ils ne font que quelques octets chacun.

Le problème!

Tout cela marche très bien en théorie, mais c'est sans compter que les programmeurs sont des êtres humains, et les êtres humains font des erreurs, surtout quand le travail devient répétitif... Hors justement, des malloc(), vous en faites des centaines, voire des milliers lorsque vous écrivez une application classique!

Inutile de vous expliquer que les causes d'erreurs sont nombreuses. Cependant, pour bien nous mettre d'accord, jetez donc un oeil au petit programme d'exemple que vous trouverez sur cette page...

#include #include #include typedef struct { int n_len; char Tc_car[]; } CHAINE; void main( void ) { char * psz = "ST Mag, c'est pas pour les enfants :-)"; CHAINE * p_chaine; /* On ne peut pas faire de sizeof( CHAINE ) ! */ p_chaine = malloc( sizeof(int) + strlen( psz ) ); p_chaine -> n_len = (int) strlen( psz ); strcpy( p_chaine -> Tc_car, psz ); printf( "\nJe dis: \"%s\" et voilà!\n", p_chaine -> Tc_car ); free( psz ); free( p_chaine ); } 

z-le donc et exécutez le! Je suis prêt à parier qu'il s'exécutera sans aucun problème! Et pourtant, il contient deux erreurs très graves!

Si le programme s'est exécuté sans problème, c'est uniquement parce qu'il est très petit et que le mal "n'a pas eu le temps d'agir!" Si les bugs vous ont déjà sauté aux yeux, je vous félicite! Mais ne pensez pas pour autant être dispensé de lire la suite, car lorsque vous travaillez sur un projet de grande envergure, vous laisserez irrémédiablement passez des bugs de la sorte... si vous vous en remettez à vos seuls yeux!

Les causes

L'auteur du programme ci-dessus à voulu stocker une chaîne de caractères, un peu comme le ferait le GfA-Basic ou le Turbo Pascal. Il a donc créé une structure CHAINE contenant d'une part la longueur de la chaîne (entier) et d'autre part les caractères composant la chaîne (tableau de caractères).

La taille de la structure CHAINE peut donc être différente selon la chaîne de caractères contenue. On ne peut donc pas utiliser sizeof( CHAINE ) et l'auteur à dû calculer "à la main", la taille de sa structure. Cette taille est naturellement la somme des tailles des éléments composant la structure, c'est à dire la taille de l'entier servant à stocker la longueur plus la taille de la chaîne.

Eh bien non, ce n'est pas si simple. En effet, l'auteur à utilisé strlen() ("string-length") pour obtenir la taille de la chaîne, mais il a ensuite utilisé strcpy() ("string-copy") pour copier la chaîne d'origine dans sa structure. Le problème, c'est que strcpy copie également le caractère NUL '\0' de fin de chaîne, or ce caractère n'est pas compté par strlen() !

Le pire, c'est que l'auteur a eu raison de copier ce fameux caractère NUL, sinon le printf() était susceptible d'afficher bien plus, que la simple chaîne de caractères passée en argument!

Par ailleurs, l'auteur consciencieux libère les zones allouées lorsqu'il n'en à plus besoin; le problème, c'est qu'il libère également une zone qui n'a pas été allouée dynamiquement (psz) !

Décidément, ce petit programme nous donnera bien du fil à retordre! Il est disponible sur la disquette dans le fichier BUGGY.C et deux corrections sont proposées dans NOBUG_1.C et NOBUG_2.C. Aucune des deux n'est parfaite, surtout vis-à-vis de l'objectif de base, mais ce n'est pas là notre propos. Nous voulons ici obtenir un moyen automatique et fiable de détecter ce genre d'erreurs. Mais pourquoi au juste?

Les conséquences!

En effet, puisque le programme d'exemple fonctionne, pourquoi s'obstiner? Eh bien, parce que ce fonctionnement n'est qu'apparent! Un coup de chance dû à sa petite taille! Voici ce qui se passe réellement...

Lorsque vous appelez malloc(), la zone de données allouée est précédée et suivie d'informations de contrôle, qui servent à toutes les routines d'allocation dynamique pour pouvoir trouver un espace libre de taille suffisante lorsqu'une nouvelle allocation a lieu. De même, lorsque vous faites un free(), ces informations sont mises à jour, pour tenir compte du fait que la zone de mémoire en question est à nouveau disponible.

Au delà de ces informations se trouvent alors les autres zones de mémoire allouées par votre programme, voire des zones de code ou des zones gérées par le système d'exploitation.

Supposons maintenant que vous écriviez des informations dans la mémoire dans une zone allouée par malloc(); tout se passe bien. Supposons maintenant que vous dépassiez de quelques octets: vous écrasez les informations de contrôle et lors de la prochaine opération de gestion dynamique de la mémoire qui lira ces informations, vous pouvez vous attendre soit à un crash, soit à une mauvaise interprétation et donc une allocation d'une nouvelle zone à un endroit réservé pour autre chose... ce qui retarde l'échéance tout en amplifiant l'ampleur de la catastrophe. C'est ce qui se passe dans notre programme lorsque nous effectuons le strcpy() qui copie le '\0' final sur les informations de contrôle et les corrompt. Heureusement, ou malheureusement, le programme s'arrête avant que la catastrophe ne survienne.

Supposons que vous écriviez bien au delà de la fin de la zone allouée, vous risquez de taper dans une autre zone et les données qui s'y trouvent s'y verront modifiées "comme par magie!". Dave Small attribuerait çà aux rayons cosmiques :-) (It's just a Joke!) Dans ce cas, vous pouvez également corrompre des données du système, et c'est à l'origine de nombreux plantages inopinés du GEM.

De même, si vous faites un free() sur une adresse mémoire qui ne correspond pas au début d'une zone mémoire précédemment allouée ou alors déjà libérée, il s'en suivra une modification de la mémoire avant et après ce que free() croit être une zone de mémoire allouée dynamiquement, et comme précédemment, la mémoire de votre ordinateur commencera à changer inopinément... C'est ce qui se passe lorsque nous faisons free( psz ) dans le petit exemple.

Le grand malheur, c'est que ces modifications involontaires sur la mémoire n'ont que très rarement un effet immédiat se traduisant par "quelques bombes" - pour une fois qu'on regrette leur absence! Au fur et à mesure que les malloc() et les free() se succéderont, la mémoire s'embrouillera et le plantage ne surviendra que bien plus tard... et sans aucune raison apparente!

La solution!

Ce genre de bugs est généralement si facile à commettre, si sournois à débugguer et si désastreux dans ses conséquences (sur une machine sans gestion de mémoire protégée - et à moins que vous n'utilisiez Mint, c'est votre cas -, la machine entière peut planter) que de nombreux langages n'implémentent pas cette possibilité. C'est notamment le cas du Basic. Même en GfA Basic, vous devez faire appel au GEMDOS pour faire un Malloc()... ou bien utiliser des string$, ce qui est encore une autre galère, due au garbage collection!

La solution que je vous propose consiste à détourner tous les appels aux fonctions de gestion de mémoire virtuelle vers des fonctions produisant des résultats en apparence identiques, mais effectuant une flopée de contrôles au passage.

Le premier contrôle, tout à fait élémentaire, consiste à vérifier qu'on ne passe pas des pointeurs NULL là où il faudrait passer l'adresse d'une zone. Dans le cas du realloc( NULL, taille ) çà peut faire particulièrement mal.

Mais, le réel moyen de contrôle consiste à allouer (de manière transparente pour vos programmes) des blocs sensiblement plus grands que ce que votre programme a demandé. On crée ainsi des petits tampons de sécurité de part et d'autre de la zone "utilisateur". Ces tampons sont remplis avec un motif binaire déterminé à l'avance (ici les mots "debu" et "fin!"). Lorsque l'on va par la suite libérer ou réallouer la zone de mémoire, on vérifiera que ces motifs binaires n'ont pas été corrompus, ce qui signifiera de manière pratiquement certaine (à une grosse coïncidence près) que vous n'avez pas légèrement débordé de l'espace mémoire que vous aviez réservé. Comme dirait l'autre: "c'est tout simple, mais il fallait y penser!".

En examinant le code fourni vous découvrirez d'autres petites astuces. Par exemple, on corrompt volontairement les tampons lors d'un free() pour détecter l'utilisation d'une zone de mémoire après sa libération, en particulier le deuxième free() vous le dira! Vous constaterez également que les tampons contiennent la taille de la zone allouée, précaution nécessaire pour pouvoir contrôler le tampon de queue par la suite...

Utilisation

L'utilisation des routines de débuggage est extrêmement simple. Il vous suffit d'inclure S_MALLOC.C dans votre projet et de rajouter la ligne:

#include "S_MALLOC.H" 

dans vos modules de code. Ensuite, remplacez tous vos appels à malloc() par la macro MALLOC(), free() par FREE(), strdup() par STRDUP(), realloc() par REALLOC() et calloc() par CALLOC().

Pour ce travail supplémentaire quasiment insignifiant, vous obtiendrez désormais un message d'erreur à l'écran à chaque fois que quelque chose semble suspect. Si nécessaire, utilisez alors votre débugger en mettant un point d'arrêt sur ces printf() et en remontant la pile pour trouver les appels et donc les zones de mémoire en cause.

Bien sûr, l'utilisation de ces routines augmente la consommation de mémoire et diminue les performances de vos applications. C'est pourquoi, une fois que vous êtes sûrs que vos malloc() sont au point, vous pouvez enlever la définition du symbole DEBUG_MALLOC dans S_MALLOC.H. Vous revenez alors immédiatement à l'utilisation des fonctions standard!

Résultats

Vous pouvez vous attendre à des résultats tout à fait surprenants en utilisant cette bibliothèque de fonctions. J'ai personnellement ainsi découvert une bonne douzaine de bugs en une après-midi, là ou j'en soupçonnais et cherchais désespérément deux ou trois depuis six mois!

Voici quelques erreurs classiques pouvant être détectées par la bibliothèque S-Malloc:

  • Index de tableaux dynamiques hors limites.
  • Mauvaise gestion des structures à taille variable.
  • Oublis du caractère NUL en fin de chaîne de caractères (vous verrez alors apparaître le mot "fin!" en fin de chaîne!)
  • Libération d'une zone de mémoire (ex: chaîne de caractères) statique!
  • free() sur une zone déjà libérée.
  • realloc() de pointeurs NULL.

Mais gardez à l'esprit que cette bibliothèque ne détectera pas toutes les erreurs, en particulier:

  • Ecriture dans une zone de mémoire voisine sans corrompre les tampons (probabilité assez faible si vous ne programmez pas avec plus de 2g d'alcool dans le sang).
  • Accès à une zone de mémoire déjà libérée. Peut être détecté par l'utilisation de MCHECK().

Voilà, n'hésitez pas à me faire part de vos expériences avec cette bibliothèque de routines qui, j'en suis sûr, va vous rendre la vie plus douce! En particulier, si vous avez d'autres méthodes pour débugguer les mallocs() ou si vous avez des idées d'amélioration de ces routines, je suis preneur!