Finding Memory leak in C++ Application on Windows

Finding memory leaks can be very difficult job sometimes, if you do not have right set of tools. My experience with memory leak detector tools on windows is bad, sometimes they are not able to correctly instrument the code and some gives you cryptic language output. So I decided to write something. After reading couple of article, in pure C++ environment you can overload operator new and operator delete and this trick can be used to build small tool which would be easy to integrate and find leaks during development. In C language world writing macros which wraps systems calls like malloc/calloc/realloc will help you to find the file name and line number . But in C++, when you do new, it first calls operator new and then the constructors, so macros or pre-processor technique would not be useful.

But Windows has nice StackWalk64 family of API which could be used to find the exact stack trace for the origin of the call.

You will need to include following header file in your project and call dumpUnfreedMemory after your application is done.

For further development on the same watch Memory Tracking On Windows

#ifndef MEMORY_TRACKER_H_
#define MEMORY_TRACKER_H_

#pragma warning( disable : 4290 )
#pragma comment(lib, "Dbghelp.lib")

#include <Windows.h>
#include <malloc.h>
#include <DbgHelp.h>
#include <stdio.h>
#include <exception>


static const int MAX_TRACES = 62;
static const int MAX_LENGTH = 256;
static const int BUFFER_LENGTH = (sizeof(SYMBOL_INFO) + MAX_LENGTH * sizeof(wchar_t) + sizeof(ULONG64) - 1) / sizeof(ULONG64); static bool SYSTEM_INITIALIZED = false;

static CRITICAL_SECTION gLock;
static bool lockInitialized = false;

void Lock() {
  if ( lockInitialized == false ) {
    ::InitializeCriticalSection(&gLock);
    lockInitialized = true;
  }
  ::EnterCriticalSection(&gLock);
}

void Unlock() {
  ::LeaveCriticalSection(&gLock);
}


typedef struct record_t {
char symbol[2048];
char filename[128];
char linenumber[2048];
  int depth;
} record;

typedef struct AllocList_t {
DWORD address;
DWORD size;
  record *details;

  struct AllocList_t* next;

} AllocList;

AllocList *gListHead = NULL;

static void GetCallStackDetails(const void* const* trace, int count, AllocList *newRecord ) {
for (int i = 0; i < count; ++i) {
ULONG64 buffer[BUFFER_LENGTH];
DWORD_PTR frame = reinterpret_cast<DWORD_PTR>(trace[i]);
DWORD64 sym_displacement = 0;
PSYMBOL_INFO symbol = reinterpret_cast<PSYMBOL_INFO>(&buffer[0]);
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
symbol->MaxNameLen = MAX_LENGTH;
BOOL has_symbol = SymFromAddr(GetCurrentProcess(), frame, &sym_displacement, symbol);
DWORD line_displacement = 0;
IMAGEHLP_LINE64 line = {};
line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
BOOL has_line = SymGetLineFromAddr64(GetCurrentProcess(), frame, &line_displacement, &line);

    _snprintf (newRecord->details[i].symbol, 2048, "%s", "(No Symbol)");

if (has_symbol) {
      _snprintf (newRecord->details[i].symbol, 2048, "%s", symbol->Name );
}
if (has_line) {
      _snprintf (newRecord->details[i].filename, 128, "%s", line.FileName);
      _snprintf (newRecord->details[i].linenumber, 2048, " [%d]", line.LineNumber);
} else {
      _snprintf (newRecord->details[i].filename, 128, "%s", "Could not determing file name");
      _snprintf (newRecord->details[i].linenumber, 2048, " [%d]", "Could not determine file line");
    }
}
}

static void addRecord(void *ptr, size_t size) {
  Lock();
if ( SYSTEM_INITIALIZED == false ) {
SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME | SYMOPT_LOAD_LINES);
if (SymInitialize(GetCurrentProcess(), NULL, TRUE)) {
SYSTEM_INITIALIZED = true;
} else {
SYSTEM_INITIALIZED = false;
return;
}
}

  AllocList *newRecord = (AllocList*) malloc ( sizeof ( AllocList ));
  newRecord->next = NULL;
  newRecord->address = (DWORD)ptr;
  newRecord->size = size;

  if ( gListHead == NULL ) {
    gListHead = newRecord;
  } else {
    AllocList *current = gListHead;
    while ( current->next != NULL ) {
      current = current->next;
    }
    current->next = newRecord;
  }

void* trace[MAX_TRACES];
int count = CaptureStackBackTrace(0, MAX_TRACES , trace, NULL);

  newRecord->details = ( record *) malloc ( count * sizeof ( record ));
  newRecord->details[0].depth = count;

GetCallStackDetails( trace, count, newRecord);
  
  Unlock();
}

static void deleteRecord(void *ptr ) {
  Lock();
  AllocList *current, *previous;
  previous = NULL;
  for (current = gListHead; current != NULL; previous = current, current = current->next) {
    if (current->address == (DWORD)ptr) {
      if (previous == NULL) {
        gListHead = current->next;
      } else {
        previous->next = current->next;
      }
      free(current);
      Unlock();
      return;
    }
  }
  Unlock();
}

void dumpUnfreedMemory(FILE *fp=stderr) {
  Lock();
  AllocList *current;
  int totalBytesNotFreed = 0;
  for (current = gListHead; current != NULL; current = current->next) {
    int depth = current->details[0].depth;
    fprintf ( fp, "Bytes allocated %d not free in following code path\n", current->size );
    totalBytesNotFreed += current->size;
    for ( int i = 0; i < depth ; i++) {
      fprintf ( fp, "%s:%s:%s\n", current->details[i].filename, current->details[i].linenumber, current->details[i].symbol);
    }
    fprintf(fp, "\n");
  }
  fprintf ( fp, "Total bytes not freed %d\n", totalBytesNotFreed );
  Unlock();
}

// Overloading new operator
void* operator new ( size_t size ) throw ( std::bad_alloc ) {
void *ptr = (void *)malloc(size);
addRecord(ptr, size);
return ptr;
}
// Overloading new[] operator
void * operator new [] (size_t size) {
  return operator new (size);
}

// Overloading delete Operator
void operator delete ( void* ptr ) throw () {
deleteRecord(ptr);
free ( ptr );
}

// Overloading delete[] Operator
void operator delete [] (void * p) {
  operator delete (p);
}

#endif

Test program

#include "MemoryTracker.h"
void dummyfunc1() {
  char *ptr = new char;
}

int *dummyfunc2() {
  int *ptr = new int;
  return ptr;
}

int main ( int argc, char **argv) {
  int *ptr  = new int;
  int *ptr1 = new int;
  int *ptr2 = new int;

  delete ptr1;
  //delete ptr2;
  //delete ptr;

  dummyfunc1();
  int *ptr3 = dummyfunc2();
  delete ptr3;

  dumpUnfreedMemory();

  return 0;
}

After running this your will get output as follows:

Bytes allocated 4 not free in following code path
memory_test\memory_test\memorytracker.h: [95]:addRecord
memory_test\memory_test\memorytracker.h: [137]:operator new
memory_test\memory_test\memory_test.cpp: [12]:main
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c: [555]:__tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c: [371]:mainCRTStartup
Could not determing file name: [18512724]:BaseThreadInitThunk
Could not determing file name: [18512724]:RtlInitializeExceptionChain
Could not determing file name: [18512724]:RtlInitializeExceptionChain

Bytes allocated 4 not free in following code path
memory_test\memory_test\memorytracker.h: [95]:addRecord
memory_test\memory_test\memorytracker.h: [137]:operator new
memory_test\memory_test\memory_test.cpp: [14]:main
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c: [555]:__tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c: [371]:mainCRTStartup
Could not determing file name: [18512724]:BaseThreadInitThunk
Could not determing file name: [18512724]:RtlInitializeExceptionChain
Could not determing file name: [18512724]:RtlInitializeExceptionChain

Bytes allocated 1 not free in following code path
memory_test\memory_test\memorytracker.h: [95]:addRecord
memory_test\memory_test\memorytracker.h: [137]:operator new
memory_test\memory_test\memory_test.cpp: [3]:dummyfunc1
memory_test\memory_test\memory_test.cpp: [21]:main
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c: [555]:__tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c: [371]:mainCRTStartup
Could not determing file name: [18512724]:BaseThreadInitThunk
Could not determing file name: [18512724]:RtlInitializeExceptionChain
Could not determing file name: [18512724]:RtlInitializeExceptionChain

Total bytes not freed 9