初识Visual Leak Detector

<!-- @page { margin: 2cm } TD P { text-align: left } H1 { line-height: 150% } H1.ctl { font-size: 14pt } A:link { so-language: zxx } -->

初识Visual Leak Detector

       灵活自由是 C/C++ 语言的一大特色,而这也为 C/C++ 程 序员出了一个难题。当程序越来越复杂时,内存的管理也会变得越加复杂,稍有不慎就会出现内存问题。内存泄漏是最常见的内存问题之一。内存泄漏如果不是很严 重,在短时间内对程序不会有太大的影响,这也使得内存泄漏问题有很强的隐蔽性,不容易被发现。然而不管内存泄漏多么轻微,当程序长时间运行时,其破坏力是 惊人的,从性能下降到内存耗尽,甚至会影响到其他程序的正常运行。另外内存问题的一个共同特点是,内存问题本身并不会有很明显的现象,当有异常现象出现时 已时过境迁,其现场已非出现问题时的现场了,这给调试内存问题带来了很大的难度。

 

       Visual Leak Detector 是一款用于 Visual C++ 的免费的内存泄露检测工具。可以在 http://www.codeproject.com/tools/visualleakdetector.asp 下载到。相比较其它的内存泄露检测工具,它在检测到内存泄漏的同时,还具有如下特点:

1   可以得到内存泄漏点的调用堆栈,如果可以的话,还可以得到其所在文件及行号;

2   可以得到泄露内存的完整数据;

3   可以设置内存泄露报告的级别;

4   它是一个已经打包的 lib ,使用时无须编译它的源代码。而对于使用者自己的代码,也只需要做很小的改动;

5   他的源代码使用 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



 


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 ,采用了新的检测机制,并在功能上有了很多改进。读者不妨体验一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值