Udostępnij za pośrednictwem


Overload Operator new to detect memory leaks

There are various leak detection methods for memory allocators. A popular one is to tag each allocation with some information about the caller. When there’s a memory leak, you just need to look at that tag info to find the line of code that allocated the memory.

 

However, this requires that the caller pass in the parameters, such as file and line number, for each allocation. (Perhaps we want to do this only on debug builds.) Also, there could be thousands of callers: we want to avoid changing all the call sites.

 

The __FILE__ and __LINE__ predefined macros and default parameters can help eliminate the need to modify thousands of lines of source code. They are the file and line number of the currently compiled line. Then we can store the file/line with every allocation.

 

We can create an allocation method with 2 default parameters, and use these predefined macros as the defaults:

 

void * MyAlloc(size_t cbSize, char *szFile = __FILE__, UINT nLineNo = __LINE__)

 

Now a caller to MyAlloc will automatically pass in the file and line number parameters with no change to the original source.

    void *ptemp = MyAlloc(100);// let's allocate 100 bytes (Default params are __FILE__ and __LINE__

 

Although this works with direct calls to MyAlloc, many allocations are via the “new” operator. We can override the default module implementation with our own:

// the single override of the module's new operator:

void * _cdecl operator new (size_t cbSize)

    void *p = MyAlloc(cbSize, __FILE__, __LINE__); // this line will show for all New operator calls<sigh>

    return p;

}

Using this technique, every “new” allocation will point to this same file/line of code. Not very useful: we want to know which line of code called “new”, not where the “new” operator override is.

 

But how do we pass in default parameters (so we don’t have to modify thousands of lines of source)?

 

If we try this:

void * _cdecl operator new (size_t cbSize, char *szFile = __FILE__, UINT nLineNo = __LINE__)

we get a linker error:

 

1>d:\dev\vc\overnew\overnew.cpp(99) : error C2668: 'operator new' : ambiguous call to overloaded function

1> d:\dev\vc\overnew\overnew.cpp(45): could be 'void *operator new(size_t,char *,UINT)'

1> d:\dev\vc\overnew\predefined c++ types (compiler internal)(23): or 'void *operator new(size_t)'

1> while trying to match the argument list '(unsigned int)'

 

That makes sense: the linker has no idea which operator new to call because there’s only 1 parameter: the size, and both implementations match operator new with 1 integer parameter.

 

We also have problems when trying to link in COM, ATL or STD Lib code which also call “new”

 

One way to avoid this is to use an overload (not an override). What’s the difference?

 

An override replaces existing functionality, whereas the overload adds functionality, and the called code is determined by it’s signature.

 

If we add a parameter to operator New (like a simple integer), then we have overloaded it. If we then modify every caller to pass in the parameter, then we can use the predefined macros and default parameter trick:

 

void * _cdecl operator new (size_t cbSize, int nAnyIntParam, char *szFile = __FILE__, UINT nLineNo = __LINE__)

 

The big drawback is we have to modify all the call sites.

 

If the call sites already have a parameter and are thus calling the overload, then no big deal: this is what the FoxPro code base does: each memory handle is marked with the kind of memory (Data Engine, Property Sheet, Form Designer, UserCode, etc). It was simple to add the File/LineNo default parameters to debug builds.

 

However, this still doesn’t work with COM, ATL, STD code. IMalloc, IMallocSpy can be used for mapping COM calls through our code.

 

A solution is to recall that the main task at hand is to find which line of code allocated a leaking block: instead of recording __LINE__, which could be the same for all new calls, let’s use the same recording mechanism to record the caller’s address (they’re both just 32 bit DWORDS.)

 

// the single override of the module's new operator:

void * _cdecl operator new (size_t cbSize)

{

    UINT *EbpRegister ; // the Base Pointer: the stack frame base

    _asm { mov EbpRegister, ebp};

    UINT CallerAddr = *(((size_t *)EbpRegister)+1) ; // the return addr

    void *p = MyAlloc(cbSize, __FILE__, CallerAddr);

    return p;

}

 

 

Below is a sample that you can build and run using Visual Studio. (I suspect all versions will work.)

 

It demonstrates allocating and freeing memory from direct calls, from operator new, and from overloaded operator new.

 

 

Start VS, choose File->New->Project->C++->Win32->Console Application. Call it OverNew

 

In the wizard choose add common header files for ATL, click Finish.

 

Paste in the Sample1 code below:

Now hit F5 to run the code.

 

If you uncomment any of the Delete lines, the program will leak: the memory is never released.

There is no warning or error that the memory leaks. In this simple scenario, the leak doesn’t matter because the process just exits. However, in more complex scenarios, the leak can be crippling.

 

 

Now when you run the code, you get output like this:

MyAlloc 1056a8 Size = 100 d:\dev\vc\overnew\overnew.cpp(23)

MyAlloc 105850 Size = 4 d:\dev\vc\overnew\overnew.cpp(61)

Op new 105958

Constructor 105958

MyAlloc 105998 Size = 4 d:\dev\vc\overnew\overnew.cpp(61)

Op new 105aa0

Constructor 105aa0

MyAlloc 105ae0 Size = 4 d:\dev\vc\overnew\overnew.cpp(69)

Op new overload 105be8

Constructor 105be8

Destructor 105958

Op del 105958

MyDelete 105958 d:\dev\vc\overnew\overnew.cpp(61)

Destructor 105aa0

Op del 105aa0

MyDelete 105aa0 d:\dev\vc\overnew\overnew.cpp(61)

Op del 1057b0

MyDelete 1057b0 d:\dev\vc\overnew\overnew.cpp(23)

Destructor 105be8

Op del 105be8

MyDelete 105be8 d:\dev\vc\overnew\overnew.cpp(69)

 

 

Note: this sample doesn’t overload the operator new []() version which allocates arrays. That’s for you to do.

void* _cdecl operator new[](size_t cbSize)

 

See also:

Examine .Net Memory Leaks

Very Advanced Debugging tips

 

 

 

<Sample1>

// OverNew.cpp : Defines the entry point for the console application.

//

#include "stdafx.h"

// version of OutputDebugString that allows variable # args

void OutputDebugStringf(char *szFmt, ...)

{

    va_list marker;

    va_start(marker, szFmt); // the varargs start at szFmt

    char szBuf[1024];

    _vsnprintf_s(szBuf, sizeof(szBuf),szFmt, marker);

    OutputDebugStringA("\n");

    OutputDebugStringA(szBuf);

}

struct AllocHeader // we'll put a header at the beginning of each alloc

{

    char szFile[MAX_PATH];

    UINT nLineNo;

};

void * MyAlloc(size_t cbSize, char *szFile = __FILE__, UINT nLineNo = __LINE__)

{

    // We allocate a header followed by the desired allocation

    void *p = malloc(sizeof(AllocHeader) + cbSize );

    AllocHeader *pHeader = (AllocHeader *)p;

    strcpy_s(pHeader->szFile, szFile);

    pHeader->nLineNo = nLineNo;

    OutputDebugStringf("MyAlloc %x Size = %d %s(%d)", p, cbSize, szFile, nLineNo);

    // we return the address + sizeof(AllocHeader)

    return (void *)( (size_t)p+sizeof(AllocHeader));

}

void MyDelete(void *p)

{

    // we need to free our allocator too

    AllocHeader *pHeader = (AllocHeader *)((size_t)p - sizeof(AllocHeader));

    OutputDebugStringf("MyDelete %x %s(%d)", p, pHeader->szFile, pHeader->nLineNo);

    free((void *)((size_t)p - sizeof(AllocHeader)));

}

// the single override of the module's new operator:

void * _cdecl operator new (size_t cbSize)

//void * _cdecl operator new (size_t cbSize, char *szFile = __FILE__, UINT nLineNo = __LINE__)

{

//#define USEWORKAROUND 1 // uncomment this to get the caller addr

#if USEWORKAROUND

    UINT *EbpRegister ; // the Base Pointer: the stack frame base

    _asm { mov EbpRegister, ebp};

    UINT CallerAddr = *(((size_t *)EbpRegister)+1) ; // the return addr

// if you get a leak, you'll get something like:

// d:\dev\vc\overnew\overnew.cpp(10189270)

// Break into the debugger. Take the # in parens, put it in Watch window: turn on hex display->it shows addr of caller

// Go to disassembly, put the address in the Address bar hit enter. Bingo: you're at the caller that didn't free!

    // CallerAddr -= (size_t)g_hinstDll; // you can get a relative address if you like: look at the link map

    void *p = MyAlloc(cbSize, __FILE__, CallerAddr);

    OutputDebugStringf("Op new %x CallerAddr = %x", p, CallerAddr);

#else

    void *p = MyAlloc(cbSize, __FILE__, __LINE__); // this line will show for all New operator calls<sigh>

    OutputDebugStringf("Op new %x", p);

#endif

    return p;

}

// an overload of the new operator, with an int param

void * _cdecl operator new (size_t cbSize, int nAnyIntParam, char *szFile = __FILE__, UINT nLineNo = __LINE__)

{

    void *p = MyAlloc(cbSize, szFile, nLineNo); // this line will show for all New operator calls<sigh>

    OutputDebugStringf("Op new overload %x", p);

    return p;

}

void  _cdecl operator delete(void *p)

{

    OutputDebugStringf("Op del %x", p);

    MyDelete(p);

}

class TestClass

{

public:

    int mem1;

    TestClass() { // Constructur

        OutputDebugStringf("Constructor %x", this);

    }

    ~TestClass() { // destructor

        OutputDebugStringf("Destructor %x", this);

    }

};

int _tmain(int argc, _TCHAR* argv[])

{

    void *ptemp = MyAlloc(100);// let's allocate 100 bytes (Default params are __FILE__ and __LINE__

    TestClass *aTestClass = new TestClass(); //allocate space for an instance

    TestClass *aTestClass2 = new TestClass(); //allocate space for another instance

    TestClass *aTestClass3 = new (1) TestClass(); //allocate space for another instance, using custom New

    delete aTestClass; // comment out this line for mem leak

    delete aTestClass2; // comment out this line for mem leak

    delete ptemp; // comment out this line for mem leak

    delete aTestClass3; // comment out this line for mem leak

    return 0;

}

</Sample1>

Comments