Visual Leak Detector工作原理(旧版本)

2024-03-28 05:18

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

这篇关于Visual Leak Detector工作原理(旧版本)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/854553

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

java程序远程debug原理与配置全过程

《java程序远程debug原理与配置全过程》文章介绍了Java远程调试的JPDA体系,包含JVMTI监控JVM、JDWP传输调试命令、JDI提供调试接口,通过-Xdebug、-Xrunjdwp参数配... 目录背景组成模块间联系IBM对三个模块的详细介绍编程使用总结背景日常工作中,每个程序员都会遇到bu

Python中isinstance()函数原理解释及详细用法示例

《Python中isinstance()函数原理解释及详细用法示例》isinstance()是Python内置的一个非常有用的函数,用于检查一个对象是否属于指定的类型或类型元组中的某一个类型,它是Py... 目录python中isinstance()函数原理解释及详细用法指南一、isinstance()函数

C#利用Free Spire.XLS for .NET复制Excel工作表

《C#利用FreeSpire.XLSfor.NET复制Excel工作表》在日常的.NET开发中,我们经常需要操作Excel文件,本文将详细介绍C#如何使用FreeSpire.XLSfor.NET... 目录1. 环境准备2. 核心功能3. android示例代码3.1 在同一工作簿内复制工作表3.2 在不同