Debugger mémoire |
Le présent document est publié sous la licence « Verbatim Copying » telle que définie par la Free Software Fondation dans le cadre du projet GNU.
Toute copie ou diffusion de ce document dans son intégralité est permise (et même encouragée), quel qu'en soit le support à la seule condition que cette notice, incluant les copyrights, soit préservée.
Copyright (C) 2004 Yann LANGLAIS, ilay.
La plus récente version en date de ce document est sise à l'url http://ilay.org/yann/articles/memdbg/.
Toute remarque, suggestion ou correction est bienvenue et peut être adressée à Yann LANGLAIS, ilay.org.
La gestion de la mémoire est toujours un exercice difficile, voir parfois périlleux. Qui n'a jamais lu cette douce phrase "segmentation fault" ou encore "core dump"?
Peut-être n'avez vous tout simplement pas remarqué? Vous n'êtes pas sur Unix? Qu'à cela ne tienne. Les écrans bleus étaient [sont encore?] là pour nous le rappeler. Qui n'a jamais eu une application disparaître purement et simplement juste avant que la sauvegarde ne soit faite?
Pour tous ces petits tracas, il n'y a pas de secret. Il faut une bonne dose de travail et d'huile de coude. Mais, lorsque certains problèmes se rencontrent souvent, on se dit : « Tiens, si j'avais un programme qui automatise tout ca... » ou encore « Il aurait tout de même pu me dire que j'avais oublié un caractère nul! ».
Quand ce genre de remarque point, il est temps de chercher l'outil qui nous manque [peut-être est-ce pour cette raison que vous me lisez?], ou, si vous êtes comme moi, une mauvaise pulsion nait en vous: vous voulez comprendre et vous ne lacherez pas le morceaux tant que vous n'aurez rien à vous mettre sous la dent.
Le but du jeu, ici, n'est pas d'obtenir un produit fini, mais de découvrir un peu les dessous des choses, et de se constituer une base de code malléable qui pourra être adaptée aux besoins.
La première des choses est en général de se poser un problème. Nous voulons nous constituer une base de débugger mémoire? Vaste sujet! voyons un peu de quoi il retourne.
Tout d'abord de qu'elle mémoire parle-t'on. Je ne vais pas m'étaler sur le sujet dans ce document, je l'ai assez fait dans « La gestion de la mémoire en C ». Aussi, je ne puis que vous prier d'y jeter un oeil si mes raccourcis vous paraissent un peu trop ... raccourcis.
La plupart des problèmes (de loin pas tous) viennent de la mémoire dynamiquement allouée. Nous allons donc oublier la pile (stack) et nous concentrer sur le tas (heap). Les variables locales, donc, nous allons les supposer exemptes d'erreur (ce n'est biensûr qu'une supposition).
Les problèmes classiques des variables du tas sont de deux types :
Pour répondre à ces deux points, nous devront mettre en place notre propre allocateur mémoire et le substituer à l'allocateur standard du système coté utilisateur (qui lui-même peut aussi être remplacer par les programmes).
Afin de remplacer l'allocateur standard, nous utiliseront les fonctionalités de « surcharge » que nous offre l'éditeur de lien à la volée (pour plus d'informations voir « Jouer avec la libdl.so ») ce qui nous permettra de ne pas avoir à recompiler les programmes à tester.
Nous ne pourrons pas faire du « générique ». En effet, à l'usage, de nombreux problèmes auxquels nous n'auront pas pensé viendront à se poser. Par exemple, tout le monde n'utilise pas la libc pour la gestion de la mémoire. Ainsi, nombre de programmes gèrent eux même leur mémoire et ce de manière parfois un peu ... « rustre ».
Autant de programmeurs, autant de pièges pour s'inserrer entre le système et le programme.
Afin de vérifier la symétrie des allocations / désallocations, il nous faur reconstruire notre propre allocateur mémoire.
L'objectif étant juste de se constituer une « boîte à outils » l'allocateur n'a pas besoin d'être complexe. Aussi, afin de diminuer la complexité, et de conserver une historique, notre allocateur ne fera que de la réservation sans jamais rien libérer.
L'allocateur sera charger de prendre en compte les désidératas du programme et de lui fournir une zone mémoire de [presque] la taille demandée. Le presque est important. En effet, afin d'éviter tous les problèmes d'aligmentent sur les architectures à cheval sur les principes, nous devrons nous débrouiller pour que chaque zone demandée commence sur une adresse mémoire divisible par la taille du bus (64bits, puisque, si l'on est pas sur, il vaut mieux trop que pas assez).
Le presque doit aussi prendre en compte les données nécessaires à l'allocateur : taille « utile » par rapport à zone réellement réservée, et pointeur vers la prochaine zone libre.
... (encore un peu de patience, merci) ...
Afin de mettre en évidence les débordements, tout en évitant les gros ennuis en cours d'execution, nous allons rajouter de part et d'autre de la zone « utile « des données de test de début et de fin de block. Nous en profiterons aussi pour réduire l'espace disponible à exactement la taille utile en inserrant des "X". Si pour un raison ou une autre, une variable dépasse d'un octet (cas des caractères de fin de chaîne), le premier "X" sera écrasé.
... (encore un peu de patience, merci) ...
Une fois ces dispositifs en place, nous aurons une meilleure visibilité sur l'utilisation de la mémoire. Cependant, ces dispositifs ne donnent qu'un instantanné et ne nous informent pas sur la dynamique. Nous pourrons savoir par exemple si une variable n'a pas été désallouée, mais nous ne saurons pas de quelle variable il s'agit.
Pour retrouver la trace de ces variables, nous allons nous appuyer sur la pile des appels de fonction d'allocation. Nous devrons stocker ces piles d'appel pour les restituer en fonction des besoins. Notre allocateur devra donc aussi être capable de réserver une plage de mémoire « interne » ou « de service ».
... (encore un peu de patience, merci) ...
L'allocateur devra prendre en charge quatre tâches :
... (encore un peu de patience, merci) ...
Le code de l'allocateur... (encore un peu de patience, merci) ...
Le code du lecteur de pile... (encore un peu de patience, merci) ...
... (encore un peu de patience, merci) ...
... (encore un peu de patience, merci) ...
... (encore un peu de patience, merci) ...