名词解释:

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下重新编译。

转载于:https://www.cnblogs.com/zd_ad/archive/2013/02/24/2923971.html

vs 2010 下使用VLD工具相关推荐

  1. bdm导入mysql_vs 2010下使用水晶报表Crystal Reports

    vs 2010下使用水晶报表Crystal Reports 的详细步骤 一.所需条件 1. 工作环境 VS 2010(页面和报表文件要在不同文件夹),对VS 2008,VS 2005 也可. 2. 数 ...

  2. word没有显示endnote_Word2007或2010未显示EndNote工具条的解决方法 | 科研动力

    Word 2007或2010未显示EndNote工具条的解決方法:1.修复安装:2.修改加载项:3.管理加载中心. 有时在Word 2007/ 2010可能会不显示EndNote工具条,造成这种情况最 ...

  3. linux stlport 编译,VC++2010下编译STLport,Boost

    VC++2010下编译STLport,Boost 最近在想向Boost转移,努力掌握Boost代码的过程中, STLport版本:5.2.1 Boost版本:1.4.6.1 (1.4.7.0也OK) ...

  4. 大学计算机基础教学大纲,《大学计算机基础》教学大纲(锦城-2010下-修改)

    <大学计算机基础>教学大纲(锦城-2010下-修改) (7页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 15.9 积分 <大学计算机基 ...

  5. vs 2010下使用水晶报表Crystal Reports

    vs 2010下使用水晶报表Crystal Reports (2013-04-26 10:34:41) vs 2010下使用水晶报表Crystal Reports 的详细步骤 一.所需条件 1. 工作 ...

  6. Linux下C++开发工具介绍

    概述     就C++开发工具而言,与Windows下微软(VC, VS2005等)一统天下相比,Linux/Unix下C++开发,可谓五花八门,各式各样.Emacs, vi, eclipse, an ...

  7. Windows 下连接mysql工具NavicatForMysql

    Windows 下连接mysql工具NavicatForMysql 一.软件介绍 Navicat for MySQL是一款强大的 MySQL 数据库管理和开发工具,它为专业开发者提供了一套强大的足够尖 ...

  8. Apache下的ArrayUtils工具类总结

    Apache下的ArrayUtils工具类总结 ArrayUtils中的方法: 1.add():将给定的数据添加到指定的数组中,返回一个新的数组. 2.addAll():合并两个数组. 3.conta ...

  9. mac下简单绘图工具

    mac下简单绘图工具 由于用的Mac系统的缘故,有时候想绘制一些简单的图形,想找一款像window下的画图工具,一直没有找到 今天无意中想到了一个方法,使用Mac系统自带的"预览" ...

最新文章

  1. itextpdf中文不显示_LaTeX实时预览中文
  2. lua学习笔试之迭代器和通用for
  3. java用重载实现获取元素的数据类型
  4. 一个不错的shell 脚本教程 入门级
  5. 《C++ Primer》14.2.1节练习
  6. Win7系统转到Win10系统的装机方法
  7. Perl多线程ping加端口扫描
  8. c语言实现61850协议,C语言实现61850库实现代码
  9. 《大数据工程师 面经 自己整理 面试题1--》
  10. 攻克银联QPBOC L2认证的最后两个不过的案例(POS与卡片的数据交互分析)
  11. 快速理解 IdentityServer4 中的认证 授权
  12. Python自动化运维之1、Python入门
  13. shopnc linux im安装教程,shopnc编译安装IM服务器node.js
  14. 管网模型(julia)
  15. 闲来无事——手势图案解屏个数
  16. 你好, View Binding! 再次再见, findViewById!
  17. 使用uber数据集和kelpergl做路网动态可视化
  18. iqoo一代充电测试软件,iqoo7充电速度测试(iqoo7充电测试)
  19. 为什么说辞职后不要回原单位工作
  20. golang解析数字证书

热门文章

  1. char和byte的区别
  2. linux基础命令练习,Linux常用命令练习
  3. php clear(),PHP Ds\Collection clear()用法及代码示例
  4. mysql 并发避免锁表_MYSQL锁表的用法,防止并发情况下的重复数据
  5. oracle form 滚动条,jQuery实现的自定义滚动条实例详解
  6. 从零开始发布前端代码到服务器上_无服务器计算:让每行代码都能住上“经济适用房”...
  7. 擅长排列的小明 II
  8. mysql orm .net,2020年 .NET ORM 完整比较、助力选择
  9. 运行时错误7内存溢出_JVM运行时内存数据区域
  10. python查询字典里的多个key_Python:如何快速找到多个字典中的公共键(key)