VC内存泄露检查工具:Visual Leak Detector

初识Visual Leak Detector
       灵活自由是C/C++语言的一大特点,而这也为C/C++程序员出了一个难题。当程序愈来愈复杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题。内存泄漏是最多见的内存问题之一。内存泄漏若是不是很严重,在短期内对程序不会有太大的影响,这也使得内存泄漏问题有很强的隐蔽性,不容易被发现。然而无论内存泄漏多么轻微,当程序长时间运行时,其破坏力是惊人的,从性能降低到内存耗尽,甚至会影响到其余程序的正常运行。另外内存问题的一个共同特色是,内存问题自己并不会有很明显的现象,当有异常现象出现时已时过境迁,其现场已非出现问题时的现场了,这给调试内存问题带来了很大的难度。
      
       Visual Leak Detector是一款用于Visual C++的免费的内存泄露检测工具。能够在 [url]http://www.codeproject.com/tools/visualleakdetector.asp[/url] 下载到。相比较其它的内存泄露检测工具,它在检测到内存泄漏的同时,还具备以下特色: 一、 能够获得内存泄漏点的调用堆栈,若是能够的话,还能够获得其所在文件及行号; 二、 能够获得泄露内存的完整数据; 三、 能够设置内存泄露报告的级别; 四、 它是一个已经打包的lib,使用时无须编译它的源代码。而对于使用者本身的代码,也只须要作很小的改动; 五、 他的源代码使用GNU许可发布,并有详尽的文档及注释。对于想深刻了解堆内存管理的读者,是一个不错的选择。               可见,从使用角度来说,Visual Leak Detector简单易用,对于使用者本身的代码,惟一的修改是#include Visual Leak Detector的头文件后正常运行本身的程序,就能够发现内存问题。从研究的角度来说,若是深刻Visual Leak Detector源代码,能够学习到堆内存分配与释放的原理、内存泄漏检测的原理及内存操做的经常使用技巧等。        本文首先将介绍Visual Leak Detector的使用方法与步骤,而后再和读者一块儿初步的研究Visual Leak Detector的源代码,去了解Visual Leak Detector的工做原理。 使用Visual Leak Detector(1.0)        下面让咱们来介绍如何使用这个小巧的工具。        首先从网站上下载zip包,解压以后获得vld.h, vldapi.h, vld.lib, vldmt.lib, vldmtdll.lib, dbghelp.dll等文件。将.h文件拷贝到Visual C++的默认include目录下,将.lib文件拷贝到Visual C++的默认lib目录下,便安装完成了。由于版本问题,若是使用windows 2000或者之前的版本,须要将dbghelp.dll拷贝到你的程序的运行目录下,或其余能够引用到的目录。        接下来须要将其加入到本身的代码中。方法很简单,只要在包含入口函数的.cpp文件中包含vld.h就能够。若是这个cpp文件包含了stdafx.h,则将包含vld.h的语句放在stdafx.h的包含语句以后,不然放在最前面。以下是一个示例程序: #include <vld.h> void main() { … }        接下来让咱们来演示如何使用Visual Leak Detector检测内存泄漏。下面是一个简单的程序,用new分配了一个int大小的堆内存,并无释放。其申请的内存地址用printf输出到屏幕上。 #include <vld.h> #include <stdlib.h> #include <stdio.h>   void f() {     int *p = new int(0x12345678);     printf("p=%08x, ", p); }   void main() {     f(); } 编译运行后,在标准输出窗口获得: p=003a89c0   在Visual C++的Output窗口获得:   WARNING: Visual Leak Detector detected memory leaks! ---------- Block 57 at 0x003A89C0: 4 bytes ---------- --57号块0x003A89C0地址泄漏了4个字节  Call Stack:                                               --下面是调用堆栈     d:\test\testvldconsole\testvldconsole\main.cpp (7): f --表示在main.cpp第7行的f()函数     d:\test\testvldconsole\testvldconsole\main.cpp (14): main –双击以引导至对应代码处     f:\rtm\vctools\crt_bld\self_x86\crt\src\crtexe.c (586): __tmainCRTStartup     f:\rtm\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): mainCRTStartup     0x7C816D4F (File and line number not available): RegisterWaitForInputIdle  Data:                                   --这是泄漏内存的内容,0x12345678     78 56 34 12                                                  xV4..... ........   Visual Leak Detector detected 1 memory leak.    第二行表示57号块有4字节的内存泄漏,地址为0x003A89C0,根据程序控制台的输出,能够知道,该地址为指针p。程序的第7行,f()函数里,在该地址处分配了4字节的堆内存空间,并赋值为0x12345678,这样在报告中,咱们看到了这4字节一样的内容。 能够看出,对于每个内存泄漏,这个报告列出了它的泄漏点、长度、分配该内存时的调用堆栈、和泄露内存的内容(分别以16进制和文本格式列出)。双击该堆栈报告的某一行,会自动在代码编辑器中跳到其所指文件的对应行。这些信息对于咱们查找内存泄露将有很大的帮助。 这是一个很方便易用的工具,安装后每次使用时,仅仅须要将它头文件包含进来从新build就能够。并且,该工具仅在build Debug版的时候会链接到你的程序中,若是build Release版,该工具不会对你的程序产生任何性能等方面影响。因此尽能够将其头文件一直包含在你的源代码中。 Visual Leak Detector工做原理        下面让咱们来看一下该工具的工做原理。        在这以前,咱们先来看一下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 FunctionTableAcce***outine,  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,采用了新的检测机制,并在功能上有了不少改进。读者不妨体验一下。
相关文章
相关标签/搜索