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

2023-11-17

原文及源码下载地址: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下重新编译。

转载于:https://www.cnblogs.com/shentao/archive/2011/10/07/2200525.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • DDD-笔记

    先说下传统系统设计 大部分从数据库开始 自底向上的设计 这种设计会使系统的设计受到数据库的影响 会有比较大的局限性 比如说 数据库仅有数据 没有行为 而对现实世界的描述则会更加抽象 更加远离业务 开发团队通过与产品或客户的沟通 直接设计表模
  • Python 快速验证代理IP是否有效

    有时候 我们需要用到代理IP 比如在爬虫的时候 但是得到了IP之后 可能不知道怎么验证这些IP是不是有效的 这时候我们可以使用Python携带该IP来模拟访问某一个网站 如果多次未成功访问 则说明这个代理是无效的 代码如下 import r
  • mysql到hive调度工具_调度工具(ETL+任务流)

    1 区别ETL作业调度工具和任务流调度工具 kettle是一个ETL工具 ETL Extract Transform Load的缩写 即数据抽取 转换 装载的过程 kettle中文名称叫水壶 该项目的主程序员MATT 希望把各种数据放到一个
  • RMAN.DBMS_RCVCAT 版本错误处理

    oracle xml oms rman target sys oracle1 emdb catalog rman rman emdb Recovery Manager Release 10 2 0 5 0 Production on Wed
  • Java中的函数使用

    Java中函数是一段可重复使用的代码块 可接受输入参数并返回结果 函数的定义通常包括函数名 参数列表和返回类型 在Java中 函数也被看作是对象 具有属性和方法 本文将从多个方面详细阐述Java中函数的使用和注意事项 一 函数的定义和使用
  • Oracle---day01

    一 简单查询语句 1 去重查询 和mysql的一样 select distinct job from emp select distinct job deptno from emp 去除job相等且deptno相等的结果 2 查询员工年薪
  • Hanlp本地化安装

    环境说明 系统 centos7 x python版本 3 9 0 这里安装完整版本hanlp full 精简版会有不少问题出现 没有找到解决方案 官网安装地址 https hanlp hankcs com install html 2 x
  • HTML简介

    目录 话不多说 先上一个HELLO WORLD 什么是 HTML HTML 标签 HTML 文档 网页 例子解释 话不多说 先上一个HELLO WORLD h1 我的第一个标题 h1 p 我的第一个段落 p 什么是 HTML HTML 是用
  • octave 机器学习_使用Octave开发机器学习算法

    octave 机器学习 Octave is an open source high level programming language designed to perform efficient numerical computation
  • 深度学习大数据

    CAFFE深度学习交流群 532629018 国内数据 链接 http pan baidu com s 1i5nyjBn 密码 26bm 好玩的数据集 链接 http pan baidu com s 1bSDIEi 密码 25zr 微软数据
  • java调用自己写的类型_Java基础——自定义类的使用

    自定义类 我们可以把类分为两种 1 一种是java中已经定义好的类 如之前用过的Scanner类 Random类 这些我们直接拿过来用就可以了 2 另一种是需要我们自己去定义的类 我们可以在类中定义多个方法和属性来供我们实际的使用 什么是类
  • Android ViewGroup提高绘制性能

    如果下面有很多子View 绘制的时候 需要开启其子View的绘制缓存功能 从而提高绘制效率 public void setChildrenDrawingCacheEnabled boolean enabled final int count
  • 全国职业院校技能大赛云计算技术与应用大赛国赛题库答案(1)

    文章目录 IaaS 云计算基础架构平台 IaaS 云平台搭建 IaaS 云平台运维 IaaS 云计算基础架构平台 IaaS 云平台搭建 1 设置主机名 防火墙设置以及 SELinux 设置如下 1 设置控制节点主机名 controller
  • 产业AI公开课正式开播!60分钟解读AI对金融科技的全新破局

    京东数科 产业AI公开课 第一季第一期 重 磅 开 播 行业热门话题 实力业内大咖 深度解读 经典对话 绝对让你这1个小时的时间欲罢不能 干货满满 从SARS到这次新冠肺炎 黑天鹅 事件对资本市场造成极大影响 不同时期的应对之道有何不同 疫
  • 欧拉函数(数论)

    include
  • 团队的远程管理_远程团队指南:如何管理您的远程软件开发团队

    团队的远程管理 Guides to help you work remotely seem to have swept through the Internet these days 这些天来 帮助您远程工作的指南似乎席卷了Internet
  • GPIO相关知识点注解

    一 GPIO工作方式 1 1 GPIO输入 输入工作方式 输入路径 输入浮空模式 I O I O I O端口 gt
  • LabVIEW组态编程的五大经验总结,助你开发过程事半功倍

    虽然NI LabVIEW软件长期以来一直帮助工程师和科学家们快速开发功能测量和控制应用 但不是所有的新用户都会遵循LabVIEW编程的最佳方法 LabVIEW图形化编程比较独特 因为只需看一眼用户的应用程序 就马上可以发现用户是否遵循编码的
  • CVPR 2023|UniDetector:7000类通用目标检测算法(港大&清华)

    作者 CV君 编辑 极市平台 点击下方卡片 关注 自动驾驶之心 公众号 ADAS巨卷干货 即可获取 点击进入 自动驾驶之心 目标检测 技术交流群 导读 论文中仅用了500个类别参与训练 就可以使UniDetector检测超过7k个类别 向大
  • Visual Leak Detector - 增强内存泄漏检测工具 for Visual C++ (翻译)

    原文及源码下载地址 http www codeproject com KB applications visualleakdetector aspx 名词解释 1 stack trace 调用堆栈信息 2 debug heap 调试堆 3