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

相关文章

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

SSID究竟是什么? WiFi网络名称及工作方式解析

《SSID究竟是什么?WiFi网络名称及工作方式解析》SID可以看作是无线网络的名称,类似于有线网络中的网络名称或者路由器的名称,在无线网络中,设备通过SSID来识别和连接到特定的无线网络... 当提到 Wi-Fi 网络时,就避不开「SSID」这个术语。简单来说,SSID 就是 Wi-Fi 网络的名称。比如

SpringCloud配置动态更新原理解析

《SpringCloud配置动态更新原理解析》在微服务架构的浩瀚星海中,服务配置的动态更新如同魔法一般,能够让应用在不重启的情况下,实时响应配置的变更,SpringCloud作为微服务架构中的佼佼者,... 目录一、SpringBoot、Cloud配置的读取二、SpringCloud配置动态刷新三、更新@R

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu

hdu4059容斥原理

求1-n中与n互质的数的4次方之和 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWrit