web-dev-qa-db-fra.com

Pourquoi le malloc ne fonctionne-t-il pas parfois?

Je porte un projet C de Linux vers Windows. Sous Linux, il est complètement stable. Sous Windows, cela fonctionne bien la plupart du temps, mais j'ai parfois un problème de segmentation.

J'utilise Microsoft Visual Studio 2010 pour compiler et déboguer et il semble que parfois mes appels malloc n'allouent tout simplement pas de mémoire, renvoyant NULL. La machine a de la mémoire libre; il a déjà traversé ce code mille fois, mais cela se produit toujours à différents endroits.

Comme je l'ai dit, cela ne se produit pas tout le temps ou au même endroit; cela ressemble à une erreur aléatoire.

Y a-t-il quelque chose que je dois faire plus attention sur Windows que sur Linux? Que puis-je faire de mal?

17
Pedro Alves

malloc() renvoie un pointeur non valide de NULL lorsqu'il est incapable de traiter une demande de mémoire. Dans la plupart des cas, les routines d'allocation de mémoire C gèrent une liste ou un tas de mémoire disponible avec des appels au système d'exploitation pour allouer des morceaux de mémoire supplémentaires lorsqu'un appel malloc() est effectué et qu'il n'y a pas de bloc sur la liste ou tas pour satisfaire la demande.

Ainsi, le premier cas d'échec de malloc() est lorsqu'une demande de mémoire ne peut pas être satisfaite car (1) il n'y a pas de bloc de mémoire utilisable dans la liste ou le tas du runtime C et (2) lorsque le runtime C la gestion de la mémoire a demandé plus de mémoire au système d'exploitation, la demande a été refusée.

Voici un article sur Stratégies d'allocation de pointeurs .

Cet article du forum donne un exemple de échec de malloc dû à la fragmentation de la mémoire .

Une autre raison pour laquelle malloc() peut échouer est que les structures de données de gestion de la mémoire sont devenues corrompues probablement en raison d'un débordement de tampon dans lequel une zone de mémoire allouée a été utilisée pour un objet plus grand que la taille de la mémoire allouée. Différentes versions de malloc() peuvent utiliser différentes stratégies pour la gestion de la mémoire et déterminer la quantité de mémoire à fournir lorsque malloc() est appelé. Par exemple, un malloc() peut vous donner exactement le nombre d'octets demandés ou il peut vous en donner plus que ce que vous avez demandé afin d'ajuster le bloc alloué dans les limites de la mémoire ou de faciliter la gestion de la mémoire.

Avec les systèmes d'exploitation modernes et la mémoire virtuelle, il est assez difficile de manquer de mémoire à moins que vous ne fassiez un stockage résident très important. Cependant, comme l'utilisateur Yeow_Meng l'a mentionné dans un commentaire ci-dessous, si vous faites de l'arithmétique pour déterminer la taille à allouer et que le résultat est un nombre négatif, vous pourriez finir par demander une énorme quantité de mémoire car l'argument à malloc() pour le la quantité de mémoire à allouer n'est pas signée.

Vous pouvez rencontrer le problème des tailles négatives lorsque vous effectuez une arithmétique de pointeur pour déterminer la quantité d'espace nécessaire pour certaines données. Ce type d'erreur est courant pour l'analyse de texte effectuée sur du texte inattendu. Par exemple, le code suivant entraînerait une très grande demande malloc().

char pathText[64] = "./dir/prefix";  // a buffer of text with path using dot (.) for current dir
char *pFile = strrchr (pathText, '/');  // find last slash where the file name begins
char *pExt = strrchr (pathText, '.');    // looking for file extension 

// at this point the programmer expected that
//   - pFile points to the last slash in the path name
//   - pExt point to the dot (.) in the file extension or NULL
// however with this data we instead have the following pointers because rather than
// an absolute path, it is a relative path
//   - pFile points to the last slash in the path name
//   - pExt point to the first dot (.) in the path name as there is no file extension
// the result is that rather than a non-NULL pExt value being larger than pFile,
// it is instead smaller for this specific data.
char *pNameNoExt;
if (pExt) {  // this really should be if (pExt && pFile < pExt) {
    // extension specified so allocate space just for the name, no extension
    // allocate space for just the file name without the extension
    // since pExt is less than pFile, we get a negative value which then becomes
    // a really huge unsigned value.
    pNameNoExt = malloc ((pExt - pFile + 1) * sizeof(char));
} else {
    pNameNoExt = malloc ((strlen(pFile) + 1) * sizeof(char));
}

Une bonne gestion de la mémoire d'exécution essaiera de fusionner des morceaux de mémoire libérés afin que de nombreux blocs plus petits soient combinés en blocs plus grands à mesure qu'ils sont libérés. Cette combinaison de blocs de mémoire réduit les chances de ne pas pouvoir répondre à une demande de mémoire à l'aide de ce qui est déjà disponible sur la liste ou le tas de mémoire géré par le temps d'exécution de gestion de la mémoire C.

Plus vous pouvez simplement réutiliser la mémoire déjà allouée et moins vous dépendez de malloc() et free(), mieux c'est. Si vous ne faites pas de malloc(), il est difficile pour lui d’échouer.

Plus vous pouvez changer de nombreux appels de petite taille en malloc() en moins d'appels volumineux en malloc(), moins vous avez de chances de fragmenter la mémoire et d'étendre la taille de la liste de mémoire ou du tas avec beaucoup de petits blocs qui ne peuvent pas être combinés car ils ne sont pas côte à côte.

Plus vous pouvez en même temps malloc() et free() blocs contigus, plus le temps d'exécution de la gestion de la mémoire peut fusionner des blocs.

Il n'y a pas de règle qui dit que vous devez faire un malloc() avec la taille spécifique d'un objet, l'argument taille fourni à malloc() peut être plus grand que la taille nécessaire pour l'objet pour lequel vous allouez Mémoire. Vous pouvez donc utiliser une sorte de règle pour les appels à malloc () afin que les blocs de taille standard soient alloués en arrondissant à une certaine quantité standard de mémoire. Vous pouvez donc allouer en blocs de 16 octets en utilisant une formule comme ((taille/16) + 1) * 16 ou plus probable ((taille >> 4) + 1) << 4. De nombreux langages de script utilisent quelque chose de similaire pour augmenter les chances d'appels répétés à malloc() et free() pour pouvoir faire correspondre une demande avec un bloc libre sur la liste ou un tas de mémoire.

Voici un exemple assez simple d'essayer de réduire le nombre de blocs alloués et désalloués. Disons que nous avons une liste chaînée de blocs de mémoire de taille variable. Ainsi, la structure des nœuds dans la liste chaînée ressemble à quelque chose comme:

typedef struct __MyNodeStruct {
    struct __MyNodeStruct *pNext;
    unsigned char *pMegaBuffer;
} MyNodeStruct;

Il pourrait y avoir deux façons d'allouer cette mémoire pour un tampon particulier et son nœud. Le premier est une allocation standard du nœud suivie d'une allocation du tampon comme ci-dessous.

MyNodeStruct *pNewNode = malloc(sizeof(MyNodeStruct));
if (pNewNode)
    pNewNode->pMegaBuffer = malloc(15000);

Cependant, une autre façon serait de faire quelque chose comme ce qui suit qui utilise une allocation de mémoire unique avec une arithmétique de pointeur afin qu'un seul malloc() fournisse les deux zones de mémoire.

MyNodeStruct *pNewNode = malloc(sizeof(myNodeStruct) + 15000);
if (pNewNode)
    pNewNode->pMegaBuffer = ((unsigned char *)pNewNode) + sizeof(myNodeStruct);

Cependant, si vous utilisez cette méthode d'allocation unique, vous devrez vous assurer que vous êtes cohérent dans l'utilisation du pointeur pMegaBuffer que vous ne faites pas accidentellement un free() dessus. Et si vous devez remplacer le tampon par un tampon plus grand, vous devrez libérer le nœud et réallouer le tampon et le nœud. Il y a donc plus de travail pour le programmeur.

28
Richard Chambers

Une autre raison d'échec de malloc() sous Windows est que votre code alloue dans un DLL et désalloue dans un autre DLL ou EXE.

Contrairement à Linux, dans Windows, un DLL ou EXE a ses propres liens vers les bibliothèques d'exécution. Cela signifie que vous pouvez lier votre programme, en utilisant le CRT 2013 à un DLL compilé avec le CRT 2008.

Les différents runtimes peuvent gérer le tas différemment. Les CRT de débogage et de libération définitivement traitent le tas différemment. Si vous malloc() dans Debug et free() dans Release, cela cassera horriblement, et cela pourrait être à l'origine de votre problème.

4
Zan Lynx