web-dev-qa-db-fra.com

Comment fonctionnent Free et Malloc en C?

J'essaie de comprendre ce qui se passerait si j'essayais de libérer un pointeur "du milieu" par exemple, regardez le code suivant:

char *ptr = (char*)malloc(10*sizeof(char));

for (char i=0 ; i<10 ; ++i)
{
    ptr[i] = i+10;
}
++ptr;
++ptr;
++ptr;
++ptr;
free(ptr);

Je reçois un crash avec un message d'erreur d'exception non gérée. Je veux comprendre pourquoi et comment fonctionne gratuitement pour que je sache non seulement comment l'utiliser, mais aussi pouvoir comprendre des erreurs et des exceptions étranges et mieux déboguer mon code ץ

Merci beaucoup

57
user238082

Lorsque vous malloc un bloc, il alloue en fait un peu plus de mémoire que vous n'en aviez demandé. Cette mémoire supplémentaire est utilisée pour stocker des informations telles que la taille du bloc alloué, et un lien vers le bloc libre/utilisé suivant dans une chaîne de blocs, et parfois des "données de garde" qui aident le système à détecter si vous écrivez la fin de votre bloc alloué. De plus, la plupart des allocateurs arrondiront la taille totale et/ou le début de votre partie de la mémoire à plusieurs octets (par exemple, sur un système 64 bits, ils peuvent aligner les données sur un multiple de 64 bits (8 octets) comme l'accès aux données à partir d'adresses non alignées peut être plus difficile et inefficace pour le processeur/bus), vous pouvez donc vous retrouver avec du "remplissage" (octets inutilisés).

Lorsque vous libérez votre pointeur, il utilise cette adresse pour trouver les informations spéciales qu'il a ajoutées au début (généralement) de votre bloc alloué. Si vous passez une adresse différente, il accèdera à la mémoire qui contient des ordures, et donc son comportement n'est pas défini (mais le plus souvent entraînera un crash)

Plus tard, si vous libérez () le bloc sans "oublier" votre pointeur, vous pouvez accidentellement essayer d'accéder aux données via ce pointeur à l'avenir, et le comportement n'est pas défini. L'une des situations suivantes peut se produire:

  • la mémoire peut être placée dans une liste de blocs libres, donc lorsque vous y accédez, elle contient toujours les données que vous y avez laissées et votre code s'exécute normalement.
  • l'allocateur de mémoire peut avoir donné (une partie de) la mémoire à une autre partie de votre programme, et cela aura probablement écrasé (certaines) vos anciennes données, donc lorsque vous les lirez, vous obtiendrez des ordures qui pourraient provoquer un comportement inattendu ou plante de votre code. Ou vous écraserez les autres données, provoquant un comportement étrange de l'autre partie de votre programme à un moment donné dans le futur.
  • la mémoire aurait pu être retournée au système d'exploitation (une "page" de mémoire que vous n'utilisez plus peut être supprimée de votre espace d'adressage, donc il n'y a plus de mémoire disponible à cette adresse - essentiellement un "trou" inutilisé dans la mémoire de votre application). Lorsque votre application tente d'accéder aux données, une erreur de mémoire dure se produit et arrête votre processus.

C'est pourquoi il est important de s'assurer que vous n'utilisez pas de pointeur après avoir libéré la mémoire vers laquelle il pointe - la meilleure pratique consiste à définir le pointeur sur NULL après avoir libéré la mémoire, car vous pouvez facilement tester NULL, et tenter d'accéder à la mémoire via un pointeur NULL entraînera un comportement incorrect mais cohérent, qui est beaucoup plus facile à déboguer.

101
Jason Williams

Vous savez probablement que vous êtes censé renvoyer exactement le pointeur que vous avez reçu.

Parce que free () ne sait pas d'abord la taille de votre bloc, il a besoin d'informations auxiliaires afin d'identifier le bloc d'origine à partir de son adresse, puis de le renvoyer à une liste gratuite. Il tentera également de fusionner les petits blocs libérés avec les voisins afin de produire un grand bloc libre plus précieux.

En fin de compte, l'allocateur doit avoir des métadonnées sur votre bloc, au minimum, il devra avoir stocké la longueur quelque part.

Je vais décrire trois façons de procéder.

  • Un endroit évident serait de le stocker juste avant le pointeur renvoyé. Il pourrait allouer un bloc de quelques octets plus grand que celui demandé, stocker la taille dans le premier mot, puis vous renvoyer un pointeur vers le deuxième mot.

  • Une autre façon serait de conserver une carte distincte décrivant au moins la longueur des blocs alloués, en utilisant l'adresse comme clé.

  • Une implémentation pourrait dériver certaines informations de l'adresse et d'autres d'une carte. L'allocateur de noyau 4.3BSD (appelé, je pense, le "allocateur McKusick-Karel" ) effectue des allocations de puissance de deux pour les objets de moins de page taille et conserve uniquement une taille par page, ce qui rend toutes les allocations à partir d'une page donnée d'une taille unique.

Il serait possible avec certains types du deuxième et probablement n'importe quel type du troisième type d'allocateur de détecter réellement que vous avez avancé le pointeur et DTRT , bien que Je doute que toute implémentation brûle le runtime pour ce faire.

25
DigitalRoss

La plupart (sinon la totalité) de l'implémentation recherchera la quantité de données pour libérer quelques octets avant le pointeur réel que vous manipulez. Faire un free sauvage entraînera une corruption de la carte mémoire.

Si votre exemple, lorsque vous allouez 10 octets de mémoire, le système réserve en fait, disons, 14. Les 4 premiers contiennent la quantité de données que vous avez demandée (10), puis la valeur de retour du malloc est un pointeur vers le premier octet de données inutilisées dans les 14 alloués.

Lorsque vous appelez free sur ce pointeur, le système recherche 4 octets en arrière pour savoir qu'il a initialement alloué 14 octets afin qu'il sache combien à libérer. Ce système vous empêche de fournir la quantité de données à libérer en tant que paramètre supplémentaire à free lui-même.

Bien sûr, une autre implémentation de malloc/free peut choisir une autre manière d'y parvenir. Mais ils ne prennent généralement pas en charge free sur un pointeur différent de ce qui a été renvoyé par malloc ou une fonction équivalente.

10
Zeograd

De http://opengroup.org/onlinepubs/007908775/xsh/free.html

La fonction free () provoque la désallocation de l'espace pointé par ptr; c'est-à-dire mis à disposition pour une allocation ultérieure. Si ptr est un pointeur nul, aucune action ne se produit. Sinon, si l'argument ne correspond pas à un pointeur précédemment renvoyé par la fonction calloc (), malloc (), realloc () ou valloc (), ou si l'espace est désalloué par un appel à free () ou realloc (), le le comportement n'est pas défini. Toute utilisation d'un pointeur qui fait référence à l'espace libéré provoque un comportement indéfini.

8
PetrosB

C'est un comportement indéfini - ne le faites pas. Seuls les pointeurs free() obtenus à partir de malloc(), ne les ajustent jamais avant cela.

Le problème est que free() doit être très rapide, donc il n'essaie pas de trouver l'allocation à laquelle appartient votre adresse ajustée, mais essaie à la place de renvoyer le bloc exactement à l'adresse ajustée dans le tas. Cela conduit à un comportement indéfini - généralement une corruption de tas ou un plantage du programme.

7
sharptooth

Vous libérez la mauvaise adresse. En modifiant la valeur de ptr, vous modifiez l'adresse. free n'a aucun moyen de savoir qu'il doit essayer de libérer un bloc commençant 4 octets en arrière. Gardez le pointeur d'origine intact et libérez-le au lieu de celui manipulé. Comme d'autres l'ont souligné, les résultats de ce que vous faites sont "indéfinis" ... d'où l'exception non gérée.

5
Jason D

Ne fais jamais ça.

Vous libérez la mauvaise adresse. En modifiant la valeur de ptr, vous modifiez l'adresse. free n'a aucun moyen de savoir qu'il doit essayer de libérer un bloc commençant 4 octets en arrière. Gardez le pointeur d'origine intact et libérez-le au lieu de celui manipulé. Comme d'autres l'ont souligné, les résultats de ce que vous faites sont "indéfinis" ... d'où l'exception non gérée

2
Jeet

Tiré du livre: Comprendre et utiliser les pointeurs C

Lorsque la mémoire est allouée, des informations supplémentaires sont stockées dans le cadre d'une structure de données gérée par le gestionnaire de segments de mémoire. Ces informations incluent, entre autres, la taille du bloc et sont généralement placées immédiatement à côté du bloc alloué.

2
Koray Tugay