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:
<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
Anonymous
January 24, 2009
I can just say one thing to this problem: Valgrind! Just doesnt work on windows :PAnonymous
March 06, 2009
The comment has been removedAnonymous
March 06, 2009
The comment has been removedAnonymous
March 22, 2009
John Robbins in his book Debugging Windows has a much more elegant solution than this.Anonymous
May 29, 2009
PingBack from http://paidsurveyshub.info/story.php?title=calvin-hsia-s-weblog-overload-operator-new-to-detect-memory-leaks