下面让我们来看一下该工具的工作原理。
在这之前,我们先来看一下
Visual C++
内置的内存泄漏检测工具是如何工作的。
Visual C++
内置的工具
CRT Debug Heap
工作原来很简单。在使用
Debug
版的
malloc
分配内存时,
malloc
会在内存块的头中记录分配该内存的文件名及行号。当程序退出时
CRT
会在
main()
函数返回之后做一些清理工作,这个时候来检查调试堆内存,如果仍然有内存没有被释放,则一定是存在内存泄漏。从这些没有被释放的内存块的头中,就可以获得文件名及行号。
这种静态的方法可以检测出内存泄漏及其泄漏点的文件名和行号,但是并不知道泄漏究竟是如何发生的,并不知道该内存分配语句是如何被执行到的。要想了解这些,就必须要对程序的内存分配过程进行动态跟踪。
Visual Leak Detector
就是这样做的。它在每次内存分配时将其上下文记录下来,当程序退出时,对于检测到的内存泄漏,查找其记录下来的上下文信息,并将其转换成报告输出。
初始化
Visual Leak Detector
要记录每一次的内存分配,而它是如何监视内存分配的呢?
Windows
提供了分配钩子
(allocation hooks)
来监视调试堆内存的分配。它是一个用户定义的回调函数,在每次从调试堆分配内存之前被调用。在初始化时,
Visual Leak Detector
使用
_CrtSetAllocHook
注册这个钩子函数,这样就可以监视从此之后所有的堆内存分配了。
如何保证在
Visual Leak Detector
初始化之前没有堆内存分配呢?全局变量是在程序启动时就初始化的,如果将
Visual Leak Detector
作为一个全局变量,就可以随程序一起启动。但是
C/C++
并没有约定全局变量之间的初始化顺序,如果其它全局变量的构造函数中有堆内存分配,则可能无法检测到。
Visual Leak Detector
使用了
C/C++
提供的
#pragma init_seg
来在某种程度上减少其它全局变量在其之前初始化的概率。根据
#pragma init_seg
的定义,全局变量的初始化分三个阶段:首先是
compiler
段,一般
c
语言的运行时库在这个时候初始化;然后是
lib
段,一般用于第三方的类库的初始化等;最后是
user
段,大部分的初始化都在这个阶段进行。
Visual Leak Detector
将其初始化设置在
compiler
段,从而使得它在绝大多数全局变量和几乎所有的用户定义的全局变量之前初始化。
记录内存分配
一个分配钩子函数需要具有如下的形式:
int
YourAllocHook( int allocType, void *userData, size_t size, int blockType, long requestNumber, const unsigned char*filename, int lineNumber);
就像前面说的,它在
Visual Leak Detector
初始化时被注册,每次从调试堆分配内存之前被调用。这个函数需要处理的事情是记录下此时的调用堆栈和此次堆内存分配的唯一标识——
requestNumber
。
得到当前的堆栈的二进制表示并不是一件很复杂的事情,但是因为不同体系结构、不同编译器、不同的函数调用约定所产生的堆栈内容略有不同,要解释堆栈并得到整个函数调用过程略显复杂。不过
windows
提供一个
StackWalk64
函数,可以获得堆栈的内容。
StackWalk64
的声明如下:
BOOL StackWalk64(
DWORD MachineType, HANDLE hProcess, HANDLE hThread, LPSTACKFRAME64 StackFrame, PVOID ContextRecord, PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine, PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine, PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress
);
STACKFRAME64
结构表示了堆栈中的一个
frame
。给出初始的
STACKFRAME64
,反复调用该函数,便可以得到内存分配点的调用堆栈了。
// Walk the stack.
while (count < _VLD_maxtraceframes) {
count++;
if (!pStackWalk64(architecture, m_process, m_thread, &frame, &context,
NULL, pSymFunctionTableAccess64, pSymGetModuleBase64, NULL)) {
// Couldn't trace back through any more frames.
break;
}
if (frame.AddrFrame.Offset == 0) {
// End of stack.
break;
}
// Push this frame's program counter onto the provided CallStack.
callstack->push_back((DWORD_PTR)frame.AddrPC.Offset);
}
那么,如何得到初始的
STACKFRAME64
结构呢?在
STACKFRAME64
结构中,其他的信息都比较容易获得,而当前的程序计数器
(EIP)
在
x86
体系结构中无法通过软件的方法直接读取。
Visual Leak Detector
使用了一种方法来获得当前的程序计数器。首先,它调用一个函数,则这个函数的返回地址就是当前的程序计数器,而函数的返回地址可以很容易的从堆栈中拿到。下面是
Visual Leak Detector
获得当前程序计数器的程序:
#if
defined(_M_IX86) || defined(_M_X64)
#pragma
auto_inline(off)
DWORD_PTR VisualLeakDetector::getprogramcounterx86x64 ()
{
DWORD_PTR programcounter;
__asm mov AXREG, [BPREG + SIZEOFPTR]
// Get the return address out of the current stack frame
__asm mov [programcounter], AXREG
// Put the return address into the variable we'll return
return programcounter;
}
#pragma
auto_inline(on)
#endif
// defined(_M_IX86) || defined(_M_X64)
得到了调用堆栈,自然要记录下来。
Visual Leak Detector
使用一个类似
map
的数据结构来记录该信息。这样可以方便的从
requestNumber
查找到其调用堆栈。分配钩子函数的
allocType
参数表示此次堆内存分配的类型,包括_HOOK_ALLOC, _HOOK_REALLOC, 和 _HOOK_FREE,下面代码是Visual Leak Detector对各种情况的处理。
switch (type) {
case _HOOK_ALLOC:
visualleakdetector.hookmalloc(request);
break;
case _HOOK_FREE:
visualleakdetector.hookfree(pdata);
break;
case _HOOK_REALLOC:
visualleakdetector.hookrealloc(pdata, request);
break;
default:
visualleakdetector.report(
"WARNING: Visual Leak Detector: in allochook(): Unhandled allocation type (%d).\n", type);
break;
}
这里,hookmalloc()函数得到当前堆栈,并将当前堆栈与requestNumber加入到类似map的数据结构中。hookfree()函数从类似map的数据结构中删除该信息。hookrealloc()函数依次调用了hookfree()和hookmalloc()。
检测内存泄露
前面提到了
Visual C++
内置的内存泄漏检测工具的工作原理。与该原理相同,因为全局变量以构造的相反顺序析构,在
Visual Leak Detector
析构时,几乎所有的其他变量都已经析构,此时如果仍然有未释放之堆内存,则必为内存泄漏。
分配的堆内存是通过一个链表来组织的,检查内存泄漏则是检查此链表。但是
windows
没有提供方法来访问这个链表。
Visual Leak Detector
使用了一个小技巧来得到它。首先在堆上申请一块临时内存,则该内存的地址可以转换成指向一个
_CrtMemBlockHeader
结构,在此结构中就可以获得这个链表。代码如下:
char *pheap =
new
char;
_CrtMemBlockHeader *pheader = pHdr(pheap)->pBlockHeaderNext;
delete
pheap;
其中pheader则为链表首指针。
报告生成
前面讲了
Visual Leak Detector
如何检测、记录内存泄漏及其其调用堆栈。但是如果要这个信息对程序员有用的话,必须转换成可读的形式。
Visual Leak Detector
使用
SymGetLineFromAddr64()
及
SymFromAddr()
生成可读的报告。
// Iterate through each frame in the call stack.
for (frame = 0; frame < callstack->size(); frame++) {
// Try to get the source file and line number associated with
// this program counter address.
if (pSymGetLineFromAddr64(m_process,
(*callstack)[frame], &displacement, &sourceinfo)) {
...
}
// Try to get the name of the function containing this program
// counter address.
if (pSymFromAddr(m_process, (*callstack)[frame],
&displacement64, pfunctioninfo)) {
functionname = pfunctioninfo->Name;
}
else {
functionname =
"(Function name unavailable)";
}
...
}
概括讲来,
Visual Leak Detector
的工作分为
3
步,首先在初始化注册一个钩子函数;然后在内存分配时该钩子函数被调用以记录下当时的现场;最后检查堆内存分配链表以确定是否存在内存泄漏并将泄漏内存的现场转换成可读的形式输出。有兴趣的读者可以阅读
Visual Leak Detector
的源代码。
总结
在使用上,
Visual Leak Detector
简单方便,结果报告一目了然。在原理上,
Visual Leak Detector
针对内存泄漏问题的特点,可谓对症下药——内存泄漏不是不容易发现吗?那就每次内存分配是都给记录下来,程序退出时算总账;内存泄漏现象出现时不是已时过境迁,并非当时泄漏点的现场了吗?那就把现场也记录下来,清清楚楚的告诉使用者那块泄漏的内存就是在如何一个调用过程中泄漏掉的。
Visual Leak Detector
是一个简单易用内存泄漏检测工具。现在最新的版本是
1.9a
,采用了新的检测机制,并在功能上有了很多改进。读者不妨体验一下。