вівторок, 1 березня 2011 р.

Memory leak detection in C++

Memory management and memory "leaks" is one of the problems, that's completely solved in languages like Java and C#. I mean in managed languages. But how about an unmanaged one, like C++?
Well, that's why C++ is unmanaged language - you have to manage everything manually. And when you do so, you can make mistakes.

The list of common mistakes made by software engineers when working with dynamic memory, includes (but is not limited to):
  • Memory leak:
    When memory block (or object) is allocated, and persist in memory even if it can't be used anymore. For example, the program looses a pointer to this memory, without notifying the system about unused memory. This results in continuous expansion of memory reserved by an application. And will definitely make any system to run out of memory (if provided enough time).
  • Memory alloc/dealloc operator mismatch:
    If you allocate with malloc/calloc, you have to call free for this pointer
    For an object, created with operator new, operator delete should be called.
    And for arrays of objects, we have new[] and delete[] operators.
    But you may run in unpredictible troubles if you do mix these types of allocation/deallocation for the same pointer.
  • Multiple deallocation of the same memory block.
  • Deallocation of memory, that was not previously allocated.
  • Access to memory outside bounds of allocated blocks.
And there are lots of chances for such things to happen in some very complex and hidden way.

There are also many ways to avoid such problems.
  • Understand C++ in the nutshell an how memory system works.
  • Use "smart" pointers
  • Use some utilities to profile and verify memory operations (there are lots of them)
There is also something you can implement in your project that will help you to fight these problems ;)
You can just overload and redefine allocation/deallocation routines and make them track lost blocks and even alloc/dealloc method mismatch.

So this is how I did it:

MLD.h:
#if !defined(MEMORY_LEAK_DETECTOR) && defined(_DEBUG)
#define MEMORY_LEAK_DETECTOR

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <memory.h>

// Standard C functions
extern "C"
{
 void* _mld_malloc(size_t aSize, const char* aFile, int aLine);
 void* _mld_calloc(size_t aNum, size_t aSize, const char* aFile, int aLine);
 void* _mld_realloc(void* aPtr, size_t aSize, const char* aFile, int aLine);
 void  _mld_free(void* aPtr, const char* aFile, int aLine);

#if !__STDC__
 char* _mld_strdup(const char* s, const char* aFile, int aLine);
#endif

 void _mld_dump();
}

#ifdef __cplusplus
// C++ global allocator/deallocator operators
extern "C++"
{
 void* operator new(size_t aSize, const char* aFile, int aLine);
 void* operator new[](size_t aSize, const char* aFile, int aLine);
 void  operator delete(void* aPtr, const char* aFile, int aLine);
 void  operator delete[](void* aPtr, const char* aFile, int aLine);
 void  operator delete(void* aPtr);
 void  operator delete[](void* aPtr);
}
#endif

#ifndef MLD_INTERNAL_LOCK
 // This should be checked to disable overloaded new and delete operators
 #define DMALLOC

 // Redefine functions to get file and line info about allocation
 #define malloc(aSize) _mld_malloc(aSize, __FILE__, __LINE__)
 #define calloc(aNum, aSize) _mld_calloc(aNum, aSize, __FILE__, __LINE__)
 #define realloc(aPtr, aSize) _mld_realloc(aPtr, aSize, __FILE__, __LINE__)
 #define free(aPtr) _mld_free(aPtr, __FILE__, __LINE__)
 #define strdup(aStr) _mld_strdup(aStr, __FILE__, __LINE__)
 #define _strdup(aStr) _mld_strdup(aStr, __FILE__, __LINE__)

 #define new new(__FILE__, __LINE__)
#endif

#endif // MEMORY_LEAK_DETECTOR




MLD.cpp:
#define MLD_INTERNAL_LOCK
#include "MemoryLeakDetector.h"

#undef malloc
#undef calloc
#undef realloc
#undef free
#undef strdup
#undef new
#undef delete

#ifdef _DEBUG

class CMld
{
public:
 enum EAllocType
 {
  eUnused,
  eMalloc,
  eNew,
  eArrayNew,

  eAllocTypeQty
 };

 struct MemoryBlockInfo
 {
  const char* mFile;
  unsigned mLine;
  size_t  mSize;
  void*  mPointer;
  EAllocType mType;

  MemoryBlockInfo()
   : mFile  ("")
   , mLine  (0)
   , mSize  (0)
   , mPointer (NULL)
   , mType  (eUnused)
  {
  }

  MemoryBlockInfo(const char* aFile, unsigned aLine, void* aPtr, size_t aSize, EAllocType aType)
   : mFile  (aFile)
   , mLine  (aLine)
   , mSize  (aSize)
   , mPointer (aPtr)
   , mType  (aType)
  {
  }
 };

 enum EConstatnts {
  eBlocksQty = 10*1024
 };

public:
 ~CMld()
 {
  Dump();
 }

 void Dump()
 {
  printf("------------  Memory Leak Detector Report  --------------\n");

  size_t totalSize = 0;
  for(int i=0; i < eBlocksQty; i++)
  {
   const MemoryBlockInfo& it = mBlocks[i];

   if(it.mType != eUnused)
   {
    totalSize += it.mSize;
    printf("%p %s:%d (%d bytes)\n", it.mPointer, it.mFile, it.mLine, it.mSize);
   }
  }
  printf("Total leaked: %d bytes\n", totalSize);

  printf("---------------------------------------------------------\n");
 }

 void TrackAllocation(const char* aFile, unsigned aLine, void* aPtr, size_t aSize, EAllocType aType)
 {
  for(int i=0; i < eBlocksQty; i++)
  {
   if(mBlocks[i].mType == eUnused)
   {
    mBlocks[i] = MemoryBlockInfo(aFile, aLine, aPtr, aSize, aType);
    return;
   }
  }
 }

 void TrackDeallocation(const char* aFile, unsigned aLine, void* aPtr, EAllocType aType)
 {
  for(int i=0; i < eBlocksQty; i++)
  {
   const MemoryBlockInfo& it = mBlocks[i];
   if(it.mType != eUnused && it.mPointer == aPtr)
   {
    if (it.mType != aType) {
     printf("Warning: Wrong alloc/free combination of address %p\n", aPtr);
     printf("   %s at %s:%d\n", AllocTypeStr[it.mType], it.mFile, it.mLine);
     printf("   %s at %s:%d\n", DeallocTypeStr[aType], aFile, aLine);
    }
    mBlocks[i].mType = eUnused;
    return;
   }
  }
  printf("Warning: Deallocation of unknown address %p\n", aPtr);
  printf("   %s at %s:%d\n", DeallocTypeStr[aType], aFile, aLine);
 }

 MemoryBlockInfo* FindBlock(void* aPtr)
 {
  for(int i=0; i < eBlocksQty; i++)
  {
   if(mBlocks[i].mPointer == aPtr)
   {
    return &mBlocks[i];
   }
  }
  return NULL;
 }
private:
 static const char* AllocTypeStr[eAllocTypeQty];
 static const char* DeallocTypeStr[eAllocTypeQty];

 MemoryBlockInfo mBlocks[eBlocksQty];

};

static CMld __mld;

const char* CMld::AllocTypeStr[eAllocTypeQty] =
{
 "unused",
 "malloc",
 "new",
 "new[]"
};

const char* CMld::DeallocTypeStr[eAllocTypeQty] =
{
 "unused",
 "free",
 "delete",
 "delete[]"
};


void* _mld_malloc(size_t aSize, const char* aFile, int aLine)
{
 void* aPtr = malloc(aSize);
 __mld.TrackAllocation(aFile, aLine, aPtr, aSize, CMld::eMalloc);
 return aPtr;
}

void* _mld_calloc(size_t aNum, size_t aSize, const char* aFile, int aLine)
{
 void* aPtr = calloc(aNum, aSize);
 __mld.TrackAllocation(aFile, aLine, aPtr, aSize * aNum, CMld::eMalloc);
 return aPtr;
}


void* _mld_realloc(void* aPtr, size_t aSize, const char* aFile, int aLine)
{
 void* nptr = realloc(aPtr, aSize);

 if(nptr!=NULL && aPtr==NULL && aSize>0) // block allocated
 {
  __mld.TrackAllocation(aFile, aLine, aPtr, aSize, CMld::eMalloc);
 }
 else if(nptr==NULL && aPtr!=NULL && aSize == 0) // block deallocated
 {
  __mld.TrackDeallocation(aFile, aLine, aPtr, CMld::eMalloc);
 }
 else if(nptr!=NULL) // block modified
 {
  CMld::MemoryBlockInfo* info = __mld.FindBlock(aPtr);
  info->mPointer = nptr;
  info->mFile = aFile;
  info->mLine = aLine;
  info->mSize = aSize;
  info->mType = CMld::eMalloc;
 }

 return nptr;
}

char* _mld_strdup(const char* str, const char* aFile, int aLine)
{
#ifdef WINCE
 char* str2 = _strdup(str);
#else
 char* str2 = strdup(str);
#endif
 __mld.TrackAllocation(aFile, aLine, str2, strlen(str), CMld::eMalloc);
 return str2;
}

void _mld_free(void* aPtr, const char* aFile, int aLine)
{
 __mld.TrackDeallocation(aFile, aLine, aPtr, CMld::eMalloc);
 free(aPtr);
}

void* operator new(size_t aSize, const char* aFile, int aLine)
{
 void* aPtr = malloc(aSize);
 __mld.TrackAllocation(aFile, aLine, aPtr, aSize, CMld::eNew);
 return aPtr;
}

void* operator new[](size_t aSize, const char* aFile, int aLine)
{
 void* aPtr = malloc(aSize);
 __mld.TrackAllocation(aFile, aLine, aPtr, aSize, CMld::eArrayNew);
 return aPtr;
}

void operator delete(void* aPtr, const char* aFile, int aLine)
{
 __mld.TrackDeallocation(aFile, aLine, aPtr, CMld::eNew);
 free(aPtr);
}
void operator delete[](void* aPtr, const char* aFile, int aLine)
{
 __mld.TrackDeallocation(aFile, aLine, aPtr, CMld::eArrayNew);
 free(aPtr);
}

void operator delete(void* aPtr)
{
 __mld.TrackDeallocation("unknown", 0, aPtr, CMld::eNew);
 free(aPtr);
}

void operator delete[](void* aPtr)
{
 __mld.TrackDeallocation("unknown", 0, aPtr, CMld::eArrayNew);
 free(aPtr);
}

void _mld_dump()
{
 __mld.Dump();
}

#endif 

Now everything you need is to include the header in every cpp file in our project. You can do it easily using a precompiled header in MSVC, or an "-include" flag for GCC.
The above example successfully handles:
  • Operator mismatches
  • Double free
  • Memory leaks
There are also some problems with this method and implementation:
  • You can see some false warnings, if you have some static objects or singletons.
  • In this implementation, I have a limited max amount of objects (can be avoided using lists)
    As you can see, there's nothing complicated here. And the above example can be extended to cover more cases and to provide more information.
    You can even make some checks for boundaries consistency! :)

    Invent! ;)

    Немає коментарів:

    Дописати коментар