Visual Leak Detector - 增强内存泄漏检测工具 for Visual C++ (翻译)

原文及源码下载地址:http://www.codeproject.com/KB/applications/visualleakdetector.aspx

 

名词解释:

1、stack trace:调用堆栈信息

2、debug heap:调试堆

3、Allocation Hook:向调试堆注册的回调函数,当申请内存时,调试堆即调用此回调函数

 

前言

VC++提供内建的内存泄漏检测,但是其功能简陋。本文介绍的工具Visual Leak Detector(以下称VLD)它提被用于替代vc++内建的检测工具,供一些特性:

1、对每个泄漏内存块提供stack trace,包括源码文件名及行数信息。

2、提供泄漏内存块的完全数据诊断(dump),包括16进制与2进制表示。

3、对于泄漏报告的细节可定制

 

vc++下可以使用的还有一些商业化的内存检测工具,例如Purify或BoundsChecker都受到人们的欢迎,但价格不菲。有相当多的免费替代品,但通常是不可靠的、有局限性的。VLD相比于其他的免费替代品有如下的优势:

1、VLD被打包成易于使用的类库。你不需要编译它的源码,只需要在你的项目中整合少许代码。

2、额外的提供stack trace,包括源码文件名,行数,和函数名,并且能提供数据诊断(data dumps)

3、支持c/c++程序(兼容new/delete 和 malloc/free)

4、提供完整的、文档化的源码,所以,你可以轻松的定制它

 

使用VLD

本节简要介绍VLD的使用基础知识。对于更深入的讨论,例如:配置选项,API,更多的高级使用场景(例如在DLL中使用),请参见位于压缩包内的完整文档。

 

欲在你的项目中使用VLD,顺序执行如下几个简单步骤即可:

1、拷贝VLD lib文件至Visual C++安装目录下的lib子文件夹内

2、拷贝VLD头文件(vld.h and vldapi.h)至Visual C++安装目录下的"include"子文件夹

3、在程序入口点所在的源文件内包含vld.h。最好将此头文件包含在其他头文件之前,stdafx.h之后,但这并不是必须的。如果这个源文件包含了stdafx.h,那么vld.h应该在其后包含。

4、如果运行环境是windows2000或更新,则需要拷贝dbghelp.dll至被调试的可执行文件目录下。

5、编译debug版本的project

 

在vc++中使用调试器运行debug版本的程序时,VLD将会启动执行。在程序结束后,内存泄漏检测报告将会显示在vc++调试信息输出窗口。双击报告中的源码行数信息,vc++将会跳转至对应的源码处。

 

注意:在release版本下,VLD并不链接到可执行文件。所以对于release版本可安全的与VLD分离。这种方式保证了不会有任何的性能下降和不良开销。

 

创建VLD

VLD的目标是成为VC++内置检测器的更好的替代品。考虑到这一点,我们使用VC++内置检测器所使用的方法,即CRT调试堆(CRT Debug Heap)。但是VLD更强大的是拥有完全的stack trace功能,它可以尽可能的帮助你找到和修正泄漏。

 

vc++内置检测器

内置检测器非常简单。当程序退出,在main返回之后,CRT执行一堆清理代码。如果内置检测器被启用,则会在清理过程中执行一些泄漏检测。泄漏检测简单的查看debug heap:如果有用户分配的内存块还存在于调试堆上,那么必然是泄漏。

调试版本的malloc调用时,会分配一个内存块头结构(block's header),其中存储着源文件名和行数。内存检测器就是简单的从头结构中取出文件名和行数,来标示一个内存泄漏信息,并将信息报告给调试器显示出来。

 

注意:内置检测器对分配和释放内存没有任何的监控。它只是简单的在进程终止前为堆生成“快照”,并且基于“快照”确定是否有泄漏发生。堆的“快照”只告诉你是否泄漏了,而不能告诉你是什么导致了泄漏。当然,要确定“是什么导致了泄漏”,我们需要得到stack trace。然而,要得到stack trace,需要在运行时监控每一次内存分配操作。这就是VLD和内置检测器的区别。

 

Allocation Hooking

幸运的是,微软提供了一种简单的方式,用于监控每一次内存分配(从调试堆中):Allocation Hook。它是一个用户提供的回调函数,此函数会在内存分配前被调用。微软提供了_CrtSetAllocHook函数,用于注册回调函数至调试堆。

调试堆调用回调函数时,会传递一个参数,参数实际是一个唯一的串号,用于标示此次分配。串号并不能为我们提供关于block's header的任何信息,但是我们可以以串号作为key,去映射对应的内存块,以记录我们想要记录的信息。

 

调用堆栈遍历(Walking the Stack)

现在我们已经可以在每次分配内存时获得通知,以及获得串号,那么现在要做的就是记录调用堆栈信息了。我们可以尝试使用内联汇编进行栈展开(unwind the stack)。但是栈帧(stack frames)的产生可能源于不同的方式,其依赖于编译器的优化和调用约定。

幸好,微软提供了函数StackWalk64,这个函数被称之为调用堆栈遍历。它在dbghelp.dll中导出。调用StackWalk64后,其会填充用户传入的STACKFRAME64结构。它可以被循环的调用,直到到达堆栈的底部。

 

初始化

现在VLD有了良好的开端。我们可以监视每一次内存分配,并且拥有stack trace。

现在只需要确保在程序启动时就为debug heap注册好回调函数。当然,这可以简单的通过创建一个全局的C++对象实例(称VLD对象)来实现,VLD对象会在程序初始化时构造。在构造时,调用_CrtSetAllocHook注册回调函数。

等等,如果程序中有其他的全局对象在构造时申请了内存,我们将如何能确保VLD对象的构造被最先调用呢?(译者注:只有VLD对象最先被调用,才能监控到其他对象的内存申请操作,包括全局对象)遗憾的是,c++规范中并没有详述任何关于全局对象构造顺序的事宜。所以,不能保证VLD对象会被最先构造。

但我们必须尽量满足这一点,我们利用一个特别的编译器预处理指令,告诉编译器,让VLD对象尽快的构造,这个指令是:#pragma init_seg (compiler)。这条指令告诉编译器,将VLD对象置入compiler段(compiler segment)。在这个段内的对象将被最先构造,接着是libray段(library segment)的对象被构造,最后是User段(user segment)的对象被构造。用户定义的全局对象默认就是置于User段。一般来说,普通的用户定义的对象是不会放入compiler段的。所以,这基本可以使我们的VLD对象在其他全局对象前构造。

 

检测内存泄漏

介于 全局对象的销毁顺序与构造顺序相反,我们的VLD对象也会在其他全局对象之后销毁。现在我们就可以像内建检测器那样检查内存泄漏了。

如果我们发现了某个内存块没有被释放,那便是一个泄漏,我们能够利用挂钩函数返回给我们的串号,来检查stack trace。STL中的map恰好合用,它可以映射串号和其stack trace。但是VLD并没有使用STL map,这是希望对旧版本的vc++保持兼容性,因为旧版本的STL并不兼容于新版本,所以不能使用它。这恰好是一个模拟STL map的好机会,并且可以在其中做特定的优化。

还记得前面提及的,内建检测器会在内存块头部取得源文件名和行数信息吗?好的,我们现在所拥有的stack trace,只是一组地址而已。将这些信息输出到调试器并不完全够用。为了让这些地址更直观,需要将它们转换为可读的信息:文件名与行数(也需要函数名)。再一次,微软带来了合适的工具帮助我们解决难题,如同StackWalk64,它们也是Debug Help Library的一部分。它们是:

1、SymGetLineFromAddr64:将给定的地址转换为源文件名和行数

2、SymFromAddr:将给定的地址转换为函数名(symbol name)

 

源码中的关键点

考虑到你可能厌倦并且跳过了前述,我将在这里进行总结。

一言以蔽之,VLD的工作过程如下:

1、首先,一个全局对象被自动创建。这个对象被最早创建。在对象的构造函数中,向调试堆注册了我们的回调函数。

2、之后,每次申请内存时都会引发回调函数被调用,回调函数中获得并记录了stack trace。这些信息被记录于类似于STL map这样的结构中。

3、最后,程序终止,这个全局对象最后被销毁。它检查调试堆并识别泄漏。泄漏的内存块在map中被查找到,其stack trace经过处理后发送至调试器并显示出来。

 

步骤1:注册Allocation Hook

这是VisualLeakDetector类的构造函数。

注意_CrtSetAllocHook的调用,allochook是我们的Allocation Hook。

linkdebughelplibrary完成了dbghelp.dll的动态链接。由于VLD自身就是一个library,隐式链接dbghelp.lib将使VLD库链接时依赖dbghelp.lib,而dbghelp.lib并非在所有的机器上都存在,同时,也是不可再发行的(not redistributable)。因此,隐式链接是不可行的。我们需要采取运行时动态链接来绕过lib。

 
// Constructor - Dynamically links with the Debug Help Library and installs the
//   allocation hook function so that the C runtime's debug heap manager will
//   call the hook function for every heap request.
VisualLeakDetector::VisualLeakDetector ()
{
    // Initialize private data.
    m_mallocmap    = new BlockMap;
    m_process      = GetCurrentProcess();
    m_selftestfile = __FILE__;
    m_status       = 0x0;
    m_thread       = GetCurrentThread();
    m_tlsindex     = TlsAlloc();
    if (_VLD_configflags & VLD_CONFIG_SELF_TEST) {
        // Self-test mode has been enabled.
        // Intentionally leak a small amount of
        // memory so that memory leak self-checking can be verified.
        strncpy(new char [21], "Memory Leak Self-Test", 21);
        m_selftestline = __LINE__;
    }
    if (m_tlsindex == TLS_OUT_OF_INDEXES) {
        report("ERROR: Visual Leak Detector:" 
               " Couldn't allocate thread local storage.\n");
    }
    else if (linkdebughelplibrary()) {
        // Register our allocation hook function with the debug heap.
        m_poldhook = _CrtSetAllocHook(allochook);
        report("Visual Leak Detector " 
               "Version "VLD_VERSION" installed ("VLD_LIBTYPE").\n");
        reportconfig();
        if (_VLD_configflags & VLD_CONFIG_START_DISABLED) {
            // Memory leak detection will initially be disabled.
 
            m_status |= VLD_STATUS_NEVER_ENABLED;
        }
        m_status |= VLD_STATUS_INSTALLED;
        return;
    }
    report("Visual Leak Detector is NOT installed!\n");
}
 

步骤2:调用堆栈遍历

这个函数承担了获取stack trace的责任,这也许是整个程序中最棘手的部分。第一次调用StackWalk64前的准备工作尤为棘手。开始之前,StackWalk64需要确切的知道从栈上的何处开始遍历,因为它并不默认从当前的栈帧(stack frame)开始遍历。这就需要我们提供当前栈帧地址以及当前程序地址(MSDN解释:此地址正是EIP中存储的地址)。可以通过GetThreadContext函数获取线程上下文,其中便包含这两个地址。但是正如MSDN的解释,GetThreadContext不能在线程运行时获取到有效的信息(据MSDN:调用前必须调用SuspendThread挂起线程)。那就是说,GetThreadContext在这里并不适用。更好的办法是直接取得所需的地址,欲达到这种效果,唯一的途径是使用内联汇编。 

获取当前栈帧地址很简单:直接从CPU的EBP寄存器中读取。

而获取程序地址则有一些困难。尽管EIP寄存器中存储了当前程序地址,但是在X86下,它不能被软件读取。那么,我们采取一种间接的方式来实现:调用另一个函数,并从此函数中获取返回地址,原理是被调用者返回地址就是调用者地址。因此,我们创建了一个特别的函数getprogramcounterx86x64。既然我们已经使用了内联汇编,那么完全可以使用汇编写一个函数调用,但是考虑到可读性,还是使用C++。

在以下的代码中,pStackWalk64、pSymFunctionTableAccess64和pSymGetModuleBase64都是函数指针,指向dbghelp.dll中的对应的API。

// getstacktrace - Traces the stack, starting from this function, as far
//   back as possible.
//  - callstack (OUT): Pointer to an empty CallStack to be populated with
//    entries from the stack trace.
//  Return Value:
//    None.
void VisualLeakDetector::getstacktrace (CallStack *callstack)
{
    DWORD        architecture;
    CONTEXT      context;
    unsigned int count = 0;
    STACKFRAME64 frame;
    DWORD_PTR    framepointer;
    DWORD_PTR    programcounter;
    // Get the required values for initialization of the STACKFRAME64 structure
    // to be passed to StackWalk64(). Required fields are AddrPC and AddrFrame.
#if defined(_M_IX86) || defined(_M_X64)
    architecture = X86X64ARCHITECTURE;
    programcounter = getprogramcounterx86x64();
    __asm mov [framepointer], BPREG // Get the frame pointer (aka base pointer)
 
#else
// If you want to retarget Visual Leak Detector to another processor
// architecture then you'll need to provide architecture-specific code to
// retrieve the current frame pointer and program counter in order to initialize
// the STACKFRAME64 structure below.
#error "Visual Leak Detector is not supported on this architecture."
#endif // defined(_M_IX86) || defined(_M_X64)
    // Initialize the STACKFRAME64 structure.
    memset(&frame, 0x0, sizeof(frame));
    frame.AddrPC.Offset    = programcounter;
    frame.AddrPC.Mode      = AddrModeFlat;
    frame.AddrFrame.Offset = framepointer;
    frame.AddrFrame.Mode   = AddrModeFlat;
    // 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);
    }
}
 
 
// getprogramcounterx86x64 - Helper function that retrieves the program counter
//   for getstacktrace() on Intel x86 or x64 architectures.
//
//  Note: Inlining of this function must be disabled. The whole purpose of this
//    function's existence depends upon it being a *called* function.
//  Return Value:
//    Returns the caller's program address.
 
#if defined(_M_IX86) || defined(_M_X64)
#pragma auto_inline(off)
DWORD_PTR VisualLeakDetector::getprogramcounterx86x64 ()
{
    DWORD_PTR programcounter;
    // Get the return address out of the current stack frame
    __asm mov AXREG, 
    // Put the return address into the variable we'll return
    __asm mov [programcounter], AXREG
 
    return programcounter;
}
#pragma auto_inline(on)
#endif // defined(_M_IX86) || defined(_M_X64)
 

步骤3:产生更好的内存泄漏报告

最后,下面的这个函数将会转换堆栈遍历时获取的程序地址至函数名。注意“地址-函数名”的转换只发生在内存泄漏被检测到的时候。避免了在程序运行时查找符号表,这将会带来巨大的额外的开销,更不必存储符号名,因为已经存储了地址,再存储符号名是没有意义的。

关于已分配的内存块链表的访问权获取,CRT并没有公布相关文档。这个链表正是被内建检测器用以确定是否存在内存泄漏。

我已经想出了关于获取链表访问权的方法。原理是:无论何时申请新的内存块,那么这个内存块都将被放置链表的头部。那么,如果要获得链表的头部,只需要临时申请一个内存块,这个临时内存块的地址可以被转换成包含_CrtMemBlockHeader结构的地址,并且拥有了链表头指针。

在以下的代码中,pSymSetOptions、pSymInitialize、pSymGetLineFromAddr64和pSymFromAddr都是函数指针,指向dbghelp.dll中导出的API。而report函数就类似于OutputDebugString这样的输出调试信息函数。

这个函数相当长,为了更好的可读性,我省略了所有的琐碎部分,以突出重点。关于函数的完全实现,请参见源码。

// reportleaks - Generates a memory leak report when the program terminates if
//   leaks were detected. The report is displayed in the debug output window.
//  Return Value:
//    None.
void VisualLeakDetector::reportleaks ()
{
    ...
    // Initialize the symbol handler. We use it for obtaining source file/line
    // number information and function names for the memory leak report.
    symbolpath = buildsymbolsearchpath();
    pSymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME);
    if (!pSymInitialize(m_process, symbolpath, TRUE)) {
        report("WARNING: Visual Leak Detector: The symbol handler" 
               " failed to initialize (error=%lu).\n"
               "    Stack traces will probably not be available" 
               " for leaked blocks.\n", GetLastError());
    }
    ...
#ifdef _MT
    _mlock(_HEAP_LOCK);
#endif // _MT
    pheap = new char;
    pheader = pHdr(pheap)->pBlockHeaderNext;
    delete pheap;
    while (pheader) {
        ...
        callstack = m_mallocmap->find(pheader->lRequest);
        if (callstack) {
            ...
            // 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)";
                }
                ...
            }
            ...
        }
        pheader = pheader->pBlockHeaderNext;
    }
#ifdef _MT
    _munlock(_HEAP_LOCK);
#endif // _MT
    ...
}
 

已知的BUG和限制

以下是最新版本的已知BUG和限制:

1、VLD不能检测COM的泄漏,out-of-process资源泄漏,或者其他一些与CRT堆无关的泄漏。简单的说,VLD只能检测new或malloc所产生的泄漏。请记住VLD的目的就是取代内建检测器,而内建检测器只检测new或malloc引起的泄漏。

2、VLD不兼容6.5版本的dbghelp.dll。建议是使用6.3版本。6.3版本已经包含在源码包内。

3、源码包内自带的预编译好的lib可能与vs2005不兼容。如果你的环境是vs2005,建议使用VLD源码在VS2005下重新编译。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值