Detecting C++ memory leaks
In this article, I'll explain how you can get a stack trace for where your resource leaks occur. This method is for Microsoft Windows. Linux developers are better served with Valgrind.
Download the source code:
The first thing we'll do is have an #ifdef, because memory tracking is inefficient. You'll want to cut it out in release versions of your code.
debug.h:
Every *.cpp source file in your program should include this file. It's optional, of course. But if you allocate something in a memory-tracked module, and free it in another that doesn't, your program will crash, since it was allocated with _dbgmalloc() and free'd with free() instead of _dbgfree().
The add_record() and del_record() functions perform the real work of memory tracking. They will allocate the requested amount of memory, but they will add space for extra tracking information. The tracking information is stored in the first few bytes of the memory block, and then the returned pointer offset by this amount. We will also reserve extra space at the end of the memory block, so we will be able to detect writes past the end of the array. We will write a specific sequence of bytes (Here, 0x12345678) at this location, and if when the block is free'd, the bytes have been modified, then your program has done something it shouldn't have, and the del_record() function will complain.
Here's the implementation for new() and delete(). They are almost the same as malloc() and free() above, except that they record the return address instead of the file and line information.
We will induce the linker to create a .map file. Add these options to your makefile when calling link.exe. Replace example with the name of your executable output file. (The debug.cpp code will assume that the map file has the same base name as the executable).
Overview
The header file
#ifdef DEBUG_MEM
#include
The implementation for malloc
void*
_dbgmalloc( const char* file, int line, size_t size )
{
void* ptr;
if ( !_init ) {
return malloc( size );
}
ptr = add_record( file, line, size );
if ( ptr == 0 ) {
dbgprint(( DMEMORY, "Out of memory." ));
return 0;
}
dbgprint(( DMEMORY, "%s:%d: malloc( %d ) [%p]", file, line, size, ptr ));
return ptr;
}
void _dbgfree( const char* file, int line, void* ptr )
{
if ( ptr == 0 ) {
return;
}
if ( !_init ) {
free( ptr );
return;
}
MemBlock* block = (MemBlock*)ptr - 1;
int size = block->size;
del_record( file, line, ptr );
dbgprint(( DMEMORY, "%s:%d: free( [%p], %d )", file, line, ptr, size ));
}
void*
add_record( const char* file, int line, size_t size )
{
MemBlock* block;
assert(_init);
block = (MemBlock*)malloc( sizeof( MemBlock ) + size + 4 );
if ( block == 0 ) {
dbgprint(( DMEMORY, "Out of memory." ));
return 0;
}
block->sentry = SENTRY;
block->size = size;
block->line = line;
block->file = _strdup( file );
if ( 0 == block->file && file ) {
free( block );
dbgprint(( DMEMORY, "Out of memory." ));
return 0;
}
memcpy( (char*)block + sizeof(*block) + size, &SENTRY,
sizeof( SENTRY ) );
EnterCriticalSection(&_cs);
list_add_tail( &_blockList, &block->list );
LeaveCriticalSection(&_cs);
return block + 1;
}
What about new?
That's all fine and good for malloc() and free(), and strdup() and _tcsdup() and calloc() and realloc(), but what about C++? When you call malloc() above, you see that the macro puts in the file and line number information, but this is not possible for the new operator. Instead, we will do it the hard way. We'll redefine the new operator and then search up the stack for the caller's address and store that. Later, we'll parse the linker's map file to figure out which function it was from the address.
void* operator new( size_t size ) throw ( std::bad_alloc )
{
static bool recurse = false;
void* ret;
CrashPosition_t pos;
if ( recurse || !_init) {
return malloc( size );
}
EnterCriticalSection(&_cs);
pos = getFileLine(1);
if ( pos.file == 0 ) {
pos.file = pos.function;
}
ret = add_record( pos.file, pos.line, size );
if ( ret == 0 ) {
dbgprint(( DMEMORY, "Out of memory." ));
LeaveCriticalSection(&_cs);
return 0;
}
dbgprint(( DMEMORY, "%s:%d: new( %d ) [%p]", pos.file, pos.line, size, ret ));
LeaveCriticalSection(&_cs);
return ret;
}
/******************************************************************************
*****************************************************************************/
void operator delete( void* ptr ) throw ()
{
CrashPosition_t pos;
if ( !_init ) {
free( ptr );
return;
}
if ( ptr == 0 ) {
return;
}
EnterCriticalSection(&_cs);
pos = getFileLine(2);
LeaveCriticalSection(&_cs);
dbgprint(( DMEMORY, "%s:%d: delete [%p]", pos.file, pos.line, ptr ));
del_record( pos.file, pos.line, ptr );
}
Walking the stack
Here's where the magic happens. Because file and line number information is not available to the new operator, we will walk the stack in order to record the return address. Later on, we'll figure out the function name where they were called from.
static int
GetCallStack( unsigned* stack, int max )
{
unsigned* my_ebp = 0;
int i;
__asm {
mov eax, ebp
mov dword ptr [my_ebp], eax;
}
// It is not safe to use this function in a WIN32 standard exception handler!
if ( IsBadReadPtr( my_ebp + 1, 4 ) ) {
return 0;
}
stack[0] = *(my_ebp + 1);
for ( i = 1; i < max; i++ ) {
unsigned addr;
if ( IsBadReadPtr( my_ebp, 4 ) ) {
break;
}
my_ebp = (unsigned*)(*my_ebp);
if ( IsBadReadPtr( my_ebp + 1, 4 ) ) {
break;
}
addr = *(my_ebp + 1);
if ( addr ) {
stack[i] = addr;
} else {
break;
}
}
return i;
}
Making the map file
So far, for malloc() and free() calls, we have recorded the file and line number information, but for new() and delete() we have only the return address. How do we figure out which function called new() and delete()?
/MAP:example.map /MAPINFO:LINES
Compiler differences
Note: For Microsoft Visual Studio 2005, Microsoft has removed the MAPINFO:LINES option. So you should either use an earlier version of the compiler, or be content without line numbers. You will still have function names.
The Map File
The Map file contains a list of every function in your program, and the exact addresses to which they are loaded. So, using a binary search, we are able to look up a function given an address. I have implemented this process in Mapfile.cpp, which is called diretly from debug.cpp.
Putting it together
When your program exits, the debug.cpp module will automatically execute this cleanup code. The cleanup code will dump out any unfree'd memory chunks.
static void
dump_blocks()
{
list_entry_t* entry = list_head( &_blockList );
while( entry != &_blockList ) {
MemBlock* block = list_entry( entry, MemBlock, list );
dbgprint(( DMEMLEAK, "Leaked %d bytes from %s:%d [%08x]",
block->size, block->file, block->line, block + 1
));
entry = entry->next;
}
if ( list_empty(&_blockList ) ) {
dbgprint(( DMEMLEAK, "No memory leaks detected." ));
}
}
dbgprintf
To see the memory leaks, you will have to implement a debug message handler. I don't have time to explain this right now, but it should be pretty obvious from the source code. Or, you can replace dbgprint() with OutputDebugString(), or printf(), or MessageBox(), or whatever you want.
Enjoy!
Another good way to avoid memory leaks is to rely more upon the RAII idiom for managing resources using objects. For example, using smart pointers.
hxxp://msdn.microsoft.com/en-us/library/e5ewb1h3(VS.80).aspx
(change hxxp to http)
Debug heap functions in MSVC can also detect buffer overrun (google for _CrtSetDbgFlag).
I'm trying to find a hidden memory leak and I'm using your code.
It's very helpful.
Thanks a lot.
The library is designed to be stripped out of DEBUG and DEBUG_MEM are not defined. In order to use it, you have to make sure DEBUG and DEBUG_MEM are defined.
But, my linker is spitting out undefined references to dbgSetHandler and dbgSetKeys.
Any ideas on what could be the problem?
Thanks.
Do you have any updated code?
Anyway excelent tool.
There is an advantage to having memory leak tracking built-in, instead of generating a huge log file with a separate tool. Whenever the debug version of my program exits, it performs the heap check, so I instantly know when and where a memory leak occurs during development. The MS UMDH tool is meant to be run when you suspect there is a problem, not all the time.
Reinvention is an important part of being a good programmer. When someone re-writes an existing software tool, the result is a tool that is a perfect fit for the purpose, plus new skills for the developer. Depending on the scope of the task, there may be a time savings over having to learn the existing tool and working around its bugs.