|
12/06/2000, Latest News And A Simple Memory Tracker
|
Wow, what an exciting month! At work, we're moving into crunch time. We
have about three months to do six months worth of work. I'm responsible for
some pretty important pieces of this project this time, and it will be
exciting to get back into that code-until-you-drop mode.
Lately, I've been working with the release candidates of DirectX 8. Some
very cool stuff is in there, and if you haven't checked it out, I highly
recommend it, even if you are an OpenGL die-hard. I've recently posted my
new DirectX 8 Graphics and Video tutorial on the net!
I've also done a lot of programming language research, much of it sparked by
Tim Sweeney. And I have started on my new game engine. Like I said, what
an exciting month.
Last time, I concluded with a play on a Sun Tsu statement, "If you know your
compiler and you know yourself, you need not fear the outcome of a thousand
battles." A very talented co-worker laughed at that statement. A few days
ago, we were solving a tricky problem with asserts, and he was amazed that
when I stopped program execution and double-clicked the entries in the call
stack, my variables would change to whatever they were before they called
the next function. Had he known this technique, he might have saved himself
tons of time.
Not only do you need to know your tools, you need the right tools in the
first place. I recently ran into a very tricky bug in DirectShow that was
uncovered by the memory tracker I was using. A memory tracker is an
invaluable tool, and I cannot understate how vital it is to good program
development. One tool you can use is BoundsChecker. BoundsChecker is a
great tool, but it is slow, and expensive, and inextensible. I don't have a
tool like this for my own, so I'll build a crude one today. Please note
that it is currently not thread safe, but it is reasonably extensible. If
you want to use it, go ahead, as I'm putting it into the public domain.
Pointers are tricky. We want to be able to gracefully catch:
1) memory leaks due to dangling pointers,
2) crashes due to freeing bogus pointers,
3) NULL pointer deletion (not usually wrong, but it is sloppy),
4) bogus pointer access / NULL pointer access.
Overriding the standard memory allocation routines can catch the first
three.
Using a smart pointer class can catch the last one.
I define a few things:
#define GlobalAllocPtr(x) DbgAlloc((x), __FILE__, __LINE__)
#define GlobalFreePtr(x) DbgFree((x), __FILE__, __LINE__)
#define malloc(x) DbgAlloc((x), __FILE__, __LINE__)
#define free(x) DbgFree((x), __FILE__, __LINE__)
|
I overload the operators new and delete:
inline void * operator new(size_t s, char * szFilename, int line) {
return DbgAlloc(s, szFilename, line);
}
inline void operator delete(void * p) {
DbgFree(p, NULL, 0);
}
#define new new(__FILE__, __LINE__)
|
and I create some memory allocation routines:
static inline void * DbgAllocRaw(size_t cbAlloc) {
// Call the Win32 function to allocate the memory.
// The funky syntax is required so that we call the _function_ and
// not some overloaded macro.
return (GlobalAlloc)(GMEM_FIXED, cbAlloc);
}
static inline void DbgFreeRaw(void * p) {
(GlobalFree)(p);
}
|
Every time an allocation is made, I save the address in a linked list. When
that memory is freed, I remove it from the list. If an allocation is made
and not freed, or if unallocated memory is freed, then a note is made in a
second linked list. When the program is done, it calls a routine to walk
the lists. If the lists show that there were errors, then a dialog is
displayed with the error type, and the file and line number that committed
the error.
The linked list is pretty basic:
struct Node
{
Node * pNext; // next node, NULL if last node
void * pMemory; // memory to track
char * szFilename; // file name that made the allocation (or bug)
int iLineNumber; // line number of allocation (or bug)
bool fBugType; // only used in bug list
};
// Two lists: an allocation tracker, and a bug tracker
static Node * g_pAllocList = NULL;
static Node * g_pBugList = NULL;
|
As you can see above, memory is allocated using the DbgAlloc and DbgFree
functions. These functions are never called directly. By using C's
malloc/free or C++'s new/delete, these routines are called automatically.
void * DbgAlloc(size_t cbAlloc, char * szFilename, int iLineNumber) {
if(0 == cbAlloc) {
assert(0);
// NOTE: If you get here, fix your code!
}
// Attempt to allocate the memory
HGLOBAL hGlobal = DbgAllocRaw(cbAlloc);
if(NULL != hGlobal) {
// Track the allocated memory
if(!ListInsert(g_pAllocList, (void *)hGlobal, szFilename, iLineNumber)) {
DbgFreeRaw(hGlobal);
hGlobal = NULL;
}
return (void *)hGlobal;
}
|
As you can see, an attempt is made to allocate memory, then the memory is
inserted into the linked list. There is some extra error detection code
thrown in there to make it a bit more robust.
void DbgFree(void * p, char * szFilename, int iLineNumber) {
if(NULL == p) {
// This is sloppy coding if you get here
return;
}
// Find and free the node
Node * fp = g_pAllocList;
Node * rp = g_pAllocList;
while((NULL != fp) && (p != fp->pMemory)) {
rp = fp;
fp = fp->pNext;
}
if(NULL != fp) {
if(g_pAllocList == fp) {
g_pAllocList = g_pAllocList->pNext;
}
else {
rp->pNext = fp->pNext;
}
DbgFreeRaw(fp);
}
// Can't find node to free
else {
// NOTE: If you get here, fix your code!
// Track the mis-freed memory
ListInsert(g_pBugList, p, szFilename, iLineNumber, true));
}
}
|
This code is a bit more complex, since it contains code to walk the linked
list. This isn't great programming design, but the point is simplicity. It
attempts to find the pointer in the list. If it finds it, it deletes it.
If it can't find it, then it inserts a node into the bug list that marks a
failure.
If you are worrying about memory trackers, you've probably seen a
linked-list. Here is the code for the insertion. I assume you can either
follow it or look it up in Sedgewick.
static bool ListInsert(Node * & pNode, void * x, char * szFilename, int LineNumber, bool fBugType) {
bool fRet = true;
do {
// Handle no current list case
if(NULL == pNode) {
pNode = (Node *)DbgAllocRaw(sizeof(Node));
if(NULL == pNode) {
fRet = false;
break;
}
pNode->pNext = NULL;
pNode->pMemory = x;
pNode->szFilename = szFilename;
pNode->iLineNumber = iLineNumber;
pNode->fBugType = fBugType;
}
// handle already in list case
else if(pNode->pMemory == x) {
fRet = false;
break;
}
// handle everything else
else {
// Search for the insertion location
Node * p = pNode;
while((NULL != p->pNext) && (p->pNext->pMemory < x)) {
p = p->pNext;
}
if((NULL != p->pNext) && (p->pNext->pMemory == x)) {
fRet = false;
break;
}
// Create the new node
Node * pT = (Node *)DbgAllocRaw(sizeof(Node));
if(NULL == pNode) {
fRet = false;
break;
}
pT->pNext = p->pNext;
pT->pMemory = x;
pT->szFilename = szFilename;
pT->iLineNumber = iLineNumber;
pT->fBugType = fBugType;
p->pNext = pT;
}
} while(0);
return fRet;
}
|
This code is fairly portable. The only routines that need replacing, so
far, are DbgAllocRaw and DbgFreeRaw. The next function also needs to be
rewritten to work on other platforms. Call this function just before you
exit to track your errors. Make sure that you don't do any allocations
after this or things might get horked.
void DumpMemoryStats() {
if(NULL != g_pAllocList || NULL != g_pBugList) {
DialogBox(NULL,
MAKEINTRESOURCE(IDD_MEMORYTRACKER),
NULL,
MemTrackerProc);
}
Node * pNode = g_pAllocList;
// Delete the alloc list
while(NULL != pNode) {
Node * pT = pNode->pNext;
DbgFreeRaw(pNode->pMemory);
DbgFreeRaw(pNode);
pNode = pT;
}
pNode = g_pBugList;
// Delete the bug list
while(NULL != pNode) {
Node * pT = pNode->pNext;
DbgFreeRaw(pNode);
pNode = pT;
}
}
|
This routine is simple in that it displays the dialog if needed, and then
clears the lists. This last major function is the DialogProc, which takes
the list information and displays it in a Windows ListView:
BOOL CALLBACK MemTrackerProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// Column definitions for statistic dumper. These can be added to as needed
static LVCOLUMN s_aCols[] = {
{ LVCF_TEXT | LVCF_WIDTH, 0, 4, "Error Type", sizeof("Error Type"), 0, 0, 0 },
{ LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM, 0, 2, "File", sizeof("File"), 1, 0, 0 },
{ LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM, 0, 4, "Line Number", sizeof("Line Number"), 2, 0, 0 },
};
switch(uMsg) {
case WM_INITDIALOG: {
HWND hwndDesc = GetDlgItem(hwndDlg, IDC_MEMORYDESCRIPTION);
RECT rc;
GetClientRect(hwndDesc, &rc);
// Setup Columns
for(int ix = 0; ix < sizeof(s_aCols) / sizeof(s_aCols[0]); ix++) {
s_aCols[ix].cx = (rc.right - rc.left) / s_aCols[ix].cx;
ListView_InsertColumn(hwndDesc, ix, s_aCols + ix);
}
// Display the remaining allocations
Node * pNode = g_pAllocList;
LVITEM item;
ZeroMemory(&item, sizeof(item));
item.mask = LVIF_TEXT;
item.pszText = "Memory Leak";
item.cchTextMax = sizeof("Memory Leak");
for(item.iItem = 0; NULL != pNode; item.iItem++, pNode = pNode->pNext) {
ListView_InsertItem(hwndDesc, &item);
// Print filename and line number
if(NULL != pNode->szFilename) {
ListView_SetItemText(hwndDesc, item.iItem, ++item.iSubItem, pNode->szFilename);
char szLine[16];
itoa(pNode->iLineNumber, szLine, 10);
ListView_SetItemText(hwndDesc, item.iItem, ++item.iSubItem, szLine);
}
}
// Display the occurred bugs
pNode = g_pBugList;
item.iSubItem = 0;
for(; NULL != pNode; item.iItem++, pNode = pNode->pNext) {
if(pNode->fBugType) {
item.pszText = "Free Bad Memory";
item.cchTextMax = sizeof("Free Bad Memory");
}
else {
item.pszText = "Smart pointer error";
item.cchTextMax = sizeof("Smart pointer error");
}
ListView_InsertItem(hwndDesc, &item);
// Print filename and line number
if(NULL != pNode->szFilename) {
ListView_SetItemText(hwndDesc, item.iItem, ++item.iSubItem, pNode->szFilename);
char szLine[16];
itoa(pNode->iLineNumber, szLine, 10);
ListView_SetItemText(hwndDesc, item.iItem, ++item.iSubItem, szLine);
}
}
return TRUE;
}
}
if(WM_COMMAND == uMsg && LOWORD(wParam) == IDCANCEL) {
EndDialog(hwndDlg, TRUE);
return TRUE;
}
return FALSE;
}
|
If you have never seen a DialogProc, this code can seem somewhat daunting.
All it does is set up a ListView control and lists memory leaks, bad
freeing, and smart pointer errors, which we discuss now.
IDC_MEMORYDESCRIPTION is the ListView control ID.
A smart pointer is a pointer container that can behave like a pointer, but
has the ability to trap errors. Here, I use a parameterized C++ class to
act as a smart pointer.
template class CP {
private:
T * p;
public:
CP() : p(NULL) {}
CP(T * pt) : p(pt) {}
CP operator=(CP cp) {
// Allow assignment of NULL pointer
if(NULL != cp.p) {
DbgVerifyPtr((T *)cp.p);
}
// Make no attempt to guard against dangling pointer
// here. The memory tracker handles that.
this->p = cp.p;
return *this;
}
T operator*() {
// Attempt to dereference if we are valid.
// It may be invalid if loaded into this container then freed.
if(DbgVerifyPtr((T *)p)) {
return *p;
}
// Invalid pointer access (logged by DbgVerifyPtr)
// Try to return gracefully
return T();
}
// There may be a time you may need this.
// However, it doesn't work for primitive
// types, so we will ignore it for now.
// T * operator->() { return p; }
void Delete(char * szFilename, int line) { DbgFree(p, szFilename, line); }
};
|
This class doesn't do much by itself. It will check if a pointer
dereference is valid. A pointer is valid if it is in our tracker list.
DbgVerifyPtr is the function that checks this. Before I give this routine,
I will finish this smart pointer with a few macros:
#define P(type) CP<type>
#define DELETE_P(x) (x.Delete(__FILE__, __LINE__))
|
This wraps the creation and deletion of pointers. Under release builds
these should be define as:
#define P(type) ((type) *)
#define DELETE_P(x) (delete (x));
|
If you need to allocate and free some memory, you might do something like:
int * p = new int(10);
delete p;
|
Under this new system, you would do:
P(int) p = new int(10);
DELETE_P(p);
|
And these will call the proper location depending on the build type.
Lastly, the DbgVerifyPtr function checks if a pointer is valid:
bool DbgVerifyPtr(void * p) {
Node * pNode = g_pAllocList;
// Search for the pointer
while(NULL != pNode) {
if(pNode->pMemory == p)
{
return true;
}
pNode = pNode->pNext;
}
// Track the mis-freed memory
ListInsert(g_pBugList, p, NULL, 0, false));
return false;
}
|
It walks the linked list and if it finds the pointer, it returns true. In
about 400 lines of code, I won't have to worry much about memory bugs. I'd
say that is one evening well spent. Its not as perfect as something like
BoundsChecker, but it can be extended to track Windows heap memory,
resources, or whatever. It also demonstrates, besides a few cool tricks, an
easy way to track resources.
Except in the case of smart pointers, just code as you normally do. If you
want to use a smart pointer, use the macro syntax to avoid having to do a
lot of conditional code.
I'll probably refine as I go along. A nice thing about debug code is that
it doesn't have to be perfect. It is meant to help the programmer. If it
starts getting in the way more than it helps, I'll get rid of it. But its
there for me (and now, everyone) to use.
-Toby
01/23/2001 - Why I Trust My Compiler
12/06/2000 - Latest News And A Simple Memory Tracker
09/21/2000 - Introduction
This document may not be reproduced in any way without
explicit permission from the author and flipCode. All Rights Reserved.
Best viewed at a high resolution.
The views expressed in this document are the views of the author and NOT neccesarily of anyone
else associated with flipCode.
|