Comment fonctionnent malloc () et free ()?

276

Je veux savoir comment mallocet freetravailler.

int main() {
    unsigned char *p = (unsigned char*)malloc(4*sizeof(unsigned char));
    memset(p,0,4);
    strcpy((char*)p,"abcdabcd"); // **deliberately storing 8bytes**
    cout << p;
    free(p); // Obvious Crash, but I need how it works and why crash.
    cout << p;
    return 0;
}

Je serais vraiment reconnaissant si la réponse est approfondie au niveau de la mémoire, si c'est possible.

mahesh
la source
5
Cela ne devrait-il pas réellement dépendre du compilateur et de la bibliothèque d'exécution utilisée?
Vilx-
9
cela dépendra de la mise en œuvre du CRT. Vous ne pouvez donc pas le généraliser.
Naveen
58
que strcpy écrit 9 octets, pas 8. N'oubliez pas le terminateur NULL ;-).
Evan Teran
2
@ LưuVĩnhPhúc c'est du C ++. Notez lecout <<
Braden Best

Réponses:

385

OK, quelques réponses sur malloc ont déjà été publiées.

La partie la plus intéressante est le fonctionnement de la gratuité (et dans ce sens, malloc aussi peut être mieux compris).

Dans de nombreuses implémentations malloc / free, free ne renvoie normalement pas la mémoire au système d'exploitation (ou du moins seulement dans de rares cas). La raison en est que vous obtiendrez des lacunes dans votre tas et donc cela peut arriver, que vous venez de terminer vos 2 ou 4 Go de mémoire virtuelle avec des lacunes. Cela devrait être évité, car dès que la mémoire virtuelle sera terminée, vous aurez de très gros ennuis. L'autre raison est que le système d'exploitation ne peut gérer que des blocs de mémoire d'une taille et d'un alignement spécifiques. Pour être précis: normalement, le système d'exploitation ne peut gérer que les blocs que le gestionnaire de mémoire virtuelle peut gérer (le plus souvent des multiples de 512 octets, par exemple 4 Ko).

Donc, retourner 40 octets au système d'exploitation ne fonctionnera tout simplement pas. Alors, que fait le libre?

Free placera le bloc de mémoire dans sa propre liste de blocs libres. Normalement, il essaie également de fusionner les blocs adjacents dans l'espace d'adressage. La liste de blocage libre n'est qu'une liste circulaire de blocs de mémoire qui contiennent au début des données administratives. C'est aussi la raison pour laquelle la gestion de très petits éléments de mémoire avec le malloc / free standard n'est pas efficace. Chaque bloc de mémoire a besoin de données supplémentaires et avec des tailles plus petites, une plus grande fragmentation se produit.

La liste libre est également le premier endroit que malloc examine lorsqu'un nouveau morceau de mémoire est nécessaire. Il est analysé avant d'appeler une nouvelle mémoire du système d'exploitation. Lorsqu'un bloc est plus grand que la mémoire requise, il est divisé en deux parties. L'un est renvoyé à l'appelant, l'autre est remis dans la liste gratuite.

Il existe de nombreuses optimisations différentes de ce comportement standard (par exemple pour les petits morceaux de mémoire). Mais comme malloc et free doivent être si universels, le comportement standard est toujours le repli lorsque les alternatives ne sont pas utilisables. Il existe également des optimisations dans la gestion de la liste libre - par exemple, le stockage des morceaux dans des listes triées par tailles. Mais toutes les optimisations ont également leurs propres limites.

Pourquoi votre code plante-t-il:

La raison en est qu'en écrivant 9 caractères (n'oubliez pas l'octet nul final) dans une zone de 4 caractères, vous écraserez probablement les données administratives stockées pour un autre bloc de mémoire qui se trouve "derrière" votre bloc de données ( car ces données sont le plus souvent stockées "devant" les morceaux de mémoire). Lorsqu'il est libre, puis essaie de mettre votre morceau dans la liste gratuite, il peut toucher ces données administratives et donc trébucher sur un pointeur écrasé. Cela fera planter le système.

C'est un comportement plutôt gracieux. J'ai également vu des situations où un pointeur en fuite quelque part a écrasé des données dans la liste sans mémoire et le système n'a pas immédiatement planté, mais certains sous-programmes plus tard. Même dans un système de complexité moyenne, de tels problèmes peuvent être vraiment, vraiment difficiles à déboguer! Dans le cas où j'étais impliqué, il nous a fallu (à un plus grand groupe de développeurs) plusieurs jours pour trouver la raison de l'accident - car il était dans un emplacement totalement différent de celui indiqué par le vidage de la mémoire. C'est comme une bombe à retardement. Vous savez, votre prochain "gratuit" ou "malloc" va planter, mais vous ne savez pas pourquoi!

Ce sont certains des pires problèmes C / C ++, et l'une des raisons pour lesquelles les pointeurs peuvent être si problématiques.

Juergen
la source
63
Trop de gens ne réalisent pas que free () peut ne pas restituer de mémoire au système d'exploitation, c'est exaspérant. Merci d'avoir aidé à les éclairer.
Artelius
Artelius: au contraire, la nouvelle volonté fait toujours?
Guillaume07
3
@ Guillaume07 Je suppose que vous vouliez dire supprimer, pas nouveau. Non, ce n'est pas (nécessairement). supprimer et faire librement (presque) la même chose. Voici le code que chacun appelle dans MSVC2013: goo.gl/3O2Kyu
Yay295
1
delete appellera toujours le destructeur, mais la mémoire elle-même peut aller sur une liste libre pour une allocation ultérieure. Selon l'implémentation, il peut même s'agir de la même liste gratuite utilisée par malloc.
David C.
1
@Juergen Mais quand free () lit des octets supplémentaires qui contiennent des informations sur la quantité de mémoire allouée par malloc, il en obtient 4. Alors comment le crash s'est-il produit ou comment free () a-t-il touché les données administratives?
Comportement
56

Comme le dit un utilisateur dans ce fil de discussion :

Votre processus a une région de mémoire, de l'adresse x à l'adresse y, appelée le tas. Toutes vos données malloc'd vivent dans ce domaine. malloc () conserve une structure de données, disons une liste, de tous les morceaux d'espace libres dans le tas. Lorsque vous appelez malloc, il recherche dans la liste un morceau qui est assez grand pour vous, lui renvoie un pointeur et enregistre le fait qu'il n'est plus gratuit ainsi que sa taille. Lorsque vous appelez free () avec le même pointeur, free () recherche la taille de ce morceau et l'ajoute à nouveau à la liste des morceaux libres (). Si vous appelez malloc () et qu'il ne trouve pas de morceau suffisamment grand dans le tas, il utilise le syscall brk () pour agrandir le tas, c'est-à-dire augmenter l'adresse y et faire en sorte que toutes les adresses entre l'ancien y et le nouveau y deviennent être une mémoire valide. brk () doit être un appel système;

malloc () dépend du système / compilateur, il est donc difficile de donner une réponse spécifique. Fondamentalement, cependant, il garde une trace de la mémoire allouée et, selon la façon dont il le fait, vos appels à libérer pourraient échouer ou réussir.

malloc() and free() don't work the same way on every O/S.

Joe
la source
1
C'est pourquoi cela s'appelle un comportement indéfini. Une implémentation pourrait faire sortir les démons de votre nez lorsque vous appelez gratuitement après une écriture invalide. On ne sait jamais.
Braden Best
36

Une implémentation de malloc / free fait ce qui suit:

  1. Obtenez un bloc de mémoire du système d'exploitation via sbrk () (appel Unix).
  2. Créez un en-tête et un pied de page autour de ce bloc de mémoire avec des informations telles que la taille, les autorisations et l'emplacement du bloc suivant et précédent.
  3. Lorsqu'un appel à malloc arrive, une liste est référencée qui pointe vers des blocs de la taille appropriée.
  4. Ce bloc est ensuite renvoyé et les en-têtes et pieds de page sont mis à jour en conséquence.
samoz
la source
25

La protection de la mémoire a une granularité de page et nécessiterait une interaction avec le noyau

Votre exemple de code demande essentiellement pourquoi l'exemple de programme ne s'interrompt pas, et la réponse est que la protection de la mémoire est une fonctionnalité du noyau et ne s'applique qu'à des pages entières, tandis que l'allocateur de mémoire est une fonctionnalité de bibliothèque et qu'il gère .. sans application .. arbitraire blocs de taille qui sont souvent beaucoup plus petits que les pages.

La mémoire ne peut être supprimée de votre programme qu'en unités de pages, et même cela est peu susceptible d'être observé.

calloc (3) et malloc (3) interagissent avec le noyau pour obtenir de la mémoire, si nécessaire. Mais la plupart des implémentations de free (3) ne renvoient pas de mémoire au noyau 1 , elles l'ajoutent simplement à une liste gratuite que calloc () et malloc () consulteront plus tard afin de réutiliser les blocs libérés.

Même si free () voulait restituer de la mémoire au système, il aurait besoin d'au moins une page de mémoire contiguë pour que le noyau protège réellement la région, donc libérer un petit bloc ne conduirait à un changement de protection que s'il était le dernier petit bloc d'une page.

Votre bloc est donc là, assis sur la liste gratuite. Vous pouvez presque toujours y accéder et la mémoire à proximité comme si elle était encore allouée. C compile directement en code machine et sans dispositions de débogage spéciales, il n'y a pas de contrôle d'intégrité sur les charges et les magasins. Maintenant, si vous essayez d'accéder à un bloc libre, le comportement n'est pas défini par la norme afin de ne pas imposer de contraintes déraisonnables aux implémenteurs de bibliothèque. Si vous essayez d'accéder à la mémoire libérée ou à la mémoire en dehors d'un bloc alloué, plusieurs choses peuvent mal se passer:

  • Parfois, les allocateurs maintiennent des blocs de mémoire séparés, parfois ils utilisent un en-tête qu'ils allouent juste avant ou après (un "pied de page", je suppose) votre bloc, mais ils peuvent simplement vouloir utiliser de la mémoire dans le bloc afin de conserver la liste gratuite liés entre eux. Si c'est le cas, votre lecture du bloc est OK, mais son contenu peut changer et l'écriture dans le bloc est susceptible d'entraîner un mauvais comportement ou un crash de l'allocateur.
  • Naturellement, votre bloc peut être alloué à l'avenir, puis il est susceptible d'être écrasé par votre code ou une routine de bibliothèque, ou avec des zéros par calloc ().
  • Si le bloc est réalloué, sa taille peut également être modifiée, auquel cas encore plus de liens ou d'initialisation seront écrits à divers endroits.
  • Évidemment, vous pouvez référencer si loin de la plage que vous traversez une frontière de l'un des segments connus du noyau de votre programme, et dans ce cas, vous allez piéger.

Théorie du fonctionnement

Ainsi, en reculant de votre exemple à la théorie générale, malloc (3) obtient la mémoire du noyau quand il en a besoin, et généralement en unités de pages. Ces pages sont divisées ou consolidées selon les besoins du programme. Malloc et free coopèrent pour maintenir un répertoire. Ils fusionnent les blocs libres adjacents lorsque cela est possible afin de pouvoir fournir de gros blocs. Le répertoire peut impliquer ou non l'utilisation de la mémoire dans des blocs libérés pour former une liste liée. (L'alternative est un peu plus de mémoire partagée et conviviale pour la pagination, et cela implique d'allouer de la mémoire spécifiquement pour le répertoire.) Malloc et free ont peu ou pas de possibilité d'imposer l'accès à des blocs individuels même lorsque du code de débogage spécial et facultatif est compilé dans le programme.


1. Le fait que très peu d'implémentations de free () tentent de restituer de la mémoire au système n'est pas nécessairement dû au ralentissement des implémenteurs. Interagir avec le noyau est beaucoup plus lent que d'exécuter simplement du code de bibliothèque, et l'avantage serait faible. La plupart des programmes ont une empreinte mémoire stable ou croissante, de sorte que le temps passé à analyser le tas à la recherche de mémoire retournable serait complètement perdu. D'autres raisons incluent le fait que la fragmentation interne rend les blocs alignés par page peu susceptibles d'exister, et il est probable que le retour d'un bloc fragmente les blocs de chaque côté. Enfin, les quelques programmes qui renvoient de grandes quantités de mémoire sont susceptibles de contourner malloc () et simplement d'allouer et de libérer des pages de toute façon.

DigitalRoss
la source
Bonne réponse. Je recommanderais l'article: Dynamic Storage Allocation: A survey and Critical review par Wilson et al pour un examen approfondi des mécanismes internes, tels que les champs d'en-tête et les listes gratuites, qui sont utilisés par les allocateurs.
Goaler444
23

En théorie, malloc obtient la mémoire du système d'exploitation pour cette application. Cependant, comme vous ne voulez que 4 octets et que le système d'exploitation doit fonctionner en pages (souvent 4k), malloc fait un peu plus que cela. Il prend une page et y met ses propres informations afin qu'il puisse garder une trace de ce que vous avez alloué et libéré de cette page.

Lorsque vous allouez 4 octets, par exemple, malloc vous donne un pointeur sur 4 octets. Ce que vous ne réalisez peut-être pas, c'est que la mémoire 8-12 octets avant vos 4 octets est utilisée par malloc pour faire une chaîne de toute la mémoire que vous avez allouée. Lorsque vous appelez gratuitement, il prend votre pointeur, sauvegarde là où se trouvent les données et fonctionne sur cela.

Lorsque vous libérez de la mémoire, malloc retire ce bloc de mémoire de la chaîne ... et peut ou non renvoyer cette mémoire au système d'exploitation. Si c'est le cas, l'accès à cette mémoire échouera probablement, car le système d'exploitation supprimera vos autorisations d'accès à cet emplacement. Si malloc conserve la mémoire (car il y a d'autres choses allouées dans cette page, ou pour une certaine optimisation), alors l'accès se passera. C'est toujours faux, mais ça pourrait marcher.

AVERTISSEMENT: Ce que j'ai décrit est une implémentation courante de malloc, mais en aucun cas la seule possible.

Chris Arguin
la source
12

Votre ligne strcpy tente de stocker 9 octets, pas 8, à cause du terminateur NUL. Il invoque un comportement indéfini.

L'appel à free peut se bloquer ou non. La mémoire "après" les 4 octets de votre allocation peut être utilisée pour autre chose par votre implémentation C ou C ++. S'il est utilisé pour autre chose, alors griffonner partout, cela "mal" va mal, mais s'il n'est pas utilisé pour autre chose, alors vous pourriez vous en tirer. "S'en tirer" peut sembler bon, mais en fait mauvais, car cela signifie que votre code semblera fonctionner correctement, mais lors d'une prochaine exécution, vous ne pourrez peut-être pas vous en sortir.

Avec un allocateur de mémoire de style débogage, vous pouvez constater qu'une valeur de garde spéciale a été écrite là-bas, et que la vérification gratuite de cette valeur et panique si elle ne la trouve pas.

Sinon, vous pourriez constater que les 5 octets suivants incluent une partie d'un nœud de liaison appartenant à un autre bloc de mémoire qui n'a pas encore été alloué. La libération de votre bloc pourrait bien impliquer de l'ajouter à une liste de blocs disponibles, et parce que vous avez griffonné dans le nœud de liste, cette opération pourrait déréférencer un pointeur avec une valeur non valide, provoquant un crash.

Tout dépend de l'allocateur de mémoire - différentes implémentations utilisent différents mécanismes.

Steve Jessop
la source
12

Le fonctionnement de malloc () et free () dépend de la bibliothèque d'exécution utilisée. En général, malloc () alloue un tas (un bloc de mémoire) à partir du système d'exploitation. Chaque requête à malloc () alloue ensuite un petit morceau de cette mémoire en renvoyant un pointeur à l'appelant. Les routines d'allocation de mémoire devront stocker des informations supplémentaires sur le bloc de mémoire alloué pour pouvoir garder une trace de la mémoire utilisée et libre sur le tas. Ces informations sont souvent stockées dans quelques octets juste avant le pointeur renvoyé par malloc () et il peut s'agir d'une liste chaînée de blocs de mémoire.

En écrivant au-delà du bloc de mémoire alloué par malloc (), vous détruisez très probablement certaines des informations de tenue de livres du bloc suivant qui peut être le bloc de mémoire inutilisé restant.

Un endroit où votre programme peut également planter est lors de la copie d'un trop grand nombre de caractères dans le tampon. Si les caractères supplémentaires se trouvent en dehors du tas, vous pouvez obtenir une violation d'accès lorsque vous essayez d'écrire dans la mémoire inexistante.

Martin Liversage
la source
6

Cela n'a rien à voir spécifiquement avec malloc et gratuit. Votre programme présente un comportement indéfini après avoir copié la chaîne - il peut se bloquer à ce stade ou à tout moment par la suite. Cela serait vrai même si vous n'avez jamais utilisé malloc et free, et alloué le tableau char sur la pile ou de manière statique.


la source
5

malloc et free dépendent de l'implémentation. Une implémentation typique implique de partitionner la mémoire disponible en une "liste libre" - une liste liée de blocs de mémoire disponibles. De nombreuses implémentations le divisent artificiellement en petits et gros objets. Les blocs libres commencent par des informations sur la taille du bloc de mémoire et où se trouve le prochain, etc.

Lorsque vous malloc, un bloc est extrait de la liste gratuite. Lorsque vous libérez, le bloc est remis dans la liste gratuite. Il y a de fortes chances que, lorsque vous écrasez la fin de votre pointeur, vous écrivez sur l'en-tête d'un bloc dans la liste libre. Lorsque vous libérez votre mémoire, free () essaie de regarder le bloc suivant et finit probablement par frapper un pointeur qui provoque une erreur de bus.

socle
la source
4

Eh bien, cela dépend de l'implémentation de l'allocateur de mémoire et du système d'exploitation.

Sous Windows par exemple, un processus peut demander une page ou plus de RAM. Le système d'exploitation affecte ensuite ces pages au processus. Il ne s'agit cependant pas de mémoire allouée à votre application. L'allocateur de mémoire CRT marquera la mémoire comme un bloc contigu "disponible". L'allocateur de mémoire CRT parcourra alors la liste des blocs libres et trouvera le plus petit bloc possible qu'il pourra utiliser. Il prendra alors autant de ce bloc que nécessaire et l'ajoutera à une liste "allouée". Un en-tête sera attaché à la tête de l'allocation de mémoire réelle. Cet en-tête contiendra divers bits d'informations (il pourrait, par exemple, contenir les blocs alloués suivants et précédents pour former une liste liée. Il contiendra très probablement la taille de l'allocation).

Free supprimera alors l'en-tête et l'ajoutera à la liste de mémoire libre. S'il forme un bloc plus grand avec les blocs libres environnants, ceux-ci seront additionnés pour donner un bloc plus grand. Si une page entière est désormais libre, l'allocateur retournera très probablement la page au système d'exploitation.

Ce n'est pas un problème simple. La portion d'allocateur de système d'exploitation est complètement hors de votre contrôle. Je vous recommande de lire quelque chose comme Malloc de Doug Lea (DLMalloc) pour comprendre comment un allocateur assez rapide fonctionnera.

Modifier: votre plantage sera causé par le fait qu'en écrivant plus que l'allocation, vous avez écrasé l'en-tête de mémoire suivant. De cette façon, quand il se libère, il est très confus quant à ce qu'il libère exactement et comment fusionner dans le bloc suivant. Cela ne peut pas toujours provoquer immédiatement un crash sur le free. Cela peut provoquer un crash plus tard. En général, évitez les remplacements de mémoire!

Goz
la source
3

Votre programme se bloque car il utilise une mémoire qui ne vous appartient pas. Il peut être utilisé par quelqu'un d'autre ou non - si vous avez de la chance, vous vous écrasez, sinon le problème peut rester caché pendant longtemps et revenir et vous mordre plus tard.

En ce qui concerne la mise en œuvre de malloc / free - des livres entiers sont consacrés au sujet. Fondamentalement, l'allocateur obtiendrait de plus gros morceaux de mémoire du système d'exploitation et les gérerait pour vous. Certains des problèmes qu'un allocateur doit résoudre sont les suivants:

  • Comment obtenir une nouvelle mémoire
  • Comment le stocker - (liste ou autre structure, plusieurs listes pour des morceaux de mémoire de taille différente, etc.)
  • Que faire si l'utilisateur demande plus de mémoire qu'actuellement disponible (demander plus de mémoire au système d'exploitation, joindre certains des blocs existants, comment les joindre exactement, ...)
  • Que faire lorsque l'utilisateur libère de la mémoire
  • Les allocateurs de débogage peuvent vous donner une plus grande partie que vous avez demandée et remplir un modèle d'octets, lorsque vous libérez de la mémoire, l'allocateur peut vérifier s'il est écrit en dehors du bloc (ce qui se produit probablement dans votre cas) ...
devdimi
la source
2

C'est difficile à dire car le comportement réel est différent entre différents compilateurs / runtimes. Même les versions de débogage / version ont un comportement différent. Les versions de débogage de VS2005 insèrent des marqueurs entre les allocations pour détecter la corruption de mémoire, donc au lieu d'un plantage, il s'affirmera dans free ().

Sebastiaan M
la source
1

Il est également important de réaliser que le simple fait de déplacer le pointeur de rupture de programme avec brket n'allouesbrk pas réellement la mémoire, il configure simplement l'espace d'adressage. Sous Linux, par exemple, la mémoire sera "sauvegardée" par des pages physiques réelles lors de l'accès à cette plage d'adresses, ce qui entraînera une erreur de page et conduira finalement le noyau à appeler l'allocateur de page pour obtenir une page de sauvegarde.

mgalgs
la source