Visual Leak Detector内存泄漏检测机制源码剖析

VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html       之前我们详细介绍了如何使用内存泄漏检测工具Visual Leak Detector(简称VLD)以及相关配置,本文我们从源码的角度去讲述VLD的内存检测原理及实现,感兴趣的朋友可以来了解一下。

1、Visual C++内置的CRT Debug Heap工作原理

       我们先来看一下Visual C++内置的CRT Debug Heap(运行时调试堆)是如何工作的。Visual C++内置的工具CRT Debug Heap工作原来很简单。比如在使用Debug版的接口动态申请内存时,会在内存块的头中记录分配该内存的文件名及行号。当程序退出时CRT会在main()函数返回之后做一些清理工作,这个时候来检查调试堆内存,如果仍然有内存没有被释放,则一定是存在内存泄漏。从这些没有被释放的内存块的头中,就可以获得文件名及行号。

       关于Visual C++内置的CRT Debug Heap调试堆的详细说明,可以参看微软官网的说明:

CRT debug heap detailshttps://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-debug-heap-details?view=msvc-170       这种静态的方法可以检测出内存泄漏及其泄漏点的文件名和行号,但是并不知道泄漏究竟是如何发生的,并不知道该内存分配语句是如何被执行到的。要想了解这些,就必须要对程序的内存分配过程进行动态跟踪。Visual Leak Detector就是这样做的。它在每次内存分配时将其上下文记录下来,当程序退出时,对于检测到的内存泄漏,查找其记录下来的上下文信息,并将其转换成报告输出。

2、VLD内存泄漏检测原理

       Visual Leak Detector的代码是开源的,有详尽的文档及注释,对于想深入了解堆内存管理以及内存泄漏排查机制的朋友,是个不错的选择。关于如何使用Visual Leak Detector,可以参见我之前写的文章:

如何使用Visual Leak Detector排查内存泄漏问题https://blog.csdn.net/chenlycly/article/details/133041372        Visual Leak Detector检测内存泄漏的大体步骤如下:

1)首先在初始化注册一个钩子函数;

2)然后在内存分配时该钩子函数被调用以记录下当时的现场;

3)最后检查堆内存分配链表以确定是 否存在内存泄漏并将泄漏内存的现场转换成可读的形式输出。

2.1、初始化

       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的定义,全局变量的初始化分三个阶段:

1)首先是compiler段,一般c语言的运行时库在这个时候初始化;

2)然后是lib段,一般用于第三方的类库的初始化等;

3)最后是user段,大部分的初始化都在这个阶段进行。

Visual Leak Detector将其初始化设置在compiler段,从而使得它在绝大多数全局变量和几乎所有的用户定义的全局变量之前初始化。

2.2、记录分配的内存

       一个分配钩子函数需要具有如下的形式:

int YourAllocHook( int allocType, void *userData, size_t size, int blockType, long requestNumber, const unsignedchar *filename, int lineNumber);

就像前面说的,它在Visual Leak Detector初始化时被注册,每次从调试堆分配内存之前被调用。这个函数需要处理的事情是记录下此时的调用堆栈和此次堆内存分配的唯一标识requestNumber。

       得到当前的堆栈的二进制表示并不是一件很复杂的事情,但是因为不同体系结构、不同编译器、不同的函数调用约定所产生的堆栈内容略有不同,要解释堆栈并得到整个函数调用过程略显复杂。不过windows提供一个StackWalk64函数,可以获得堆栈的内容。StackWalk64的声明如下:

BOOL IMAGEAPI StackWalk64([in]           DWORD                            MachineType,[in]           HANDLE                           hProcess,[in]           HANDLE                           hThread,[in, out]      LPSTACKFRAME64                   StackFrame,[in, out]      PVOID                            ContextRecord,[in, optional] PREAD_PROCESS_MEMORY_ROUTINE64   ReadMemoryRoutine,[in, optional] PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine,[in, optional] PGET_MODULE_BASE_ROUTINE64       GetModuleBaseRoutine,[in, optional] PTRANSLATE_ADDRESS_ROUTINE64     TranslateAddress
);

STACKFRAME64结构表示了堆栈中的一个frame,该结构体定义如下:

typedef struct _tagSTACKFRAME64 {ADDRESS64 AddrPC;ADDRESS64 AddrReturn;ADDRESS64 AddrFrame;ADDRESS64 AddrStack;ADDRESS64 AddrBStore;PVOID     FuncTableEntry;DWORD64   Params[4];BOOL      Far;BOOL      Virtual;DWORD64   Reserved[3];KDHELP64  KdHelp;
} STACKFRAME64, *LPSTACKFRAME64;

给出初始的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 returnreturn 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()。

2.3、检测内存泄露

       前面提到了Visual C++内置的内存泄漏检测工具的工作原理。与该原理相同,因为全局变量以构造的相反顺序析构,在Visual Leak Detector析构时,几乎所有的其他变量都已经析构,此时如果仍然有未释放之堆内存,则必为内存泄漏。

       分配的堆内存是通过一个链表来组织的,检查内存泄漏则是检查此链表。但是windows没有提供方法来访问这个链表。Visual Leak Detector使用了一个小技巧来得到它。首先在堆上申请一块临时内存,则该内存的地址可以转换成指向一个_CrtMemBlockHeader结构,在此结构中就可以获得这个链表。代码如下:

char *pheap = newchar;
_CrtMemBlockHeader *pheader = pHdr(pheap)->pBlockHeaderNext;
delete pheap;

其中pheader则为链表首指针。

2.4、生成检测报告

       前面讲了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步:

1)首先在初始化注册一个钩子函数;

2)然后在内存分配时该钩子函数被调用以记录下当时的现场;

3)最后检查堆内存分配链表以确定是否存在内存泄漏并将泄漏内存的现场转换成可读的形式输出。

详细的细节,有兴趣的读者可以阅读Visual Leak Detector的源代码。 

       比如我故意写了一段内存泄漏的代码,Visual Leak Detector生成的报告内容如下:

Detected memory leaks!
Dumping objects ->
d:\testmemleak\testmemleak\testmemleak.cpp(70) : {343} normal block at 0x00C1E3A8, 2000 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 13 at 0x00C1E3A8: 2000 bytes ----------
  Leak Hash: 0xDA40455C, Count: 1, Total 2000 bytes

Call Stack (TID 4356):
    mfc100ud.dll!0x7B874750()
    d:\testmemleak\testmemleak\testmemleak.cpp (70): TestMemLeak.exe!CTestMemLeakApp::InitInstance() + 0x18 bytes
    mfc100ud.dll!0x7BBA94F4()
    f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\appmodul.cpp (26): TestMemLeak.exe!wWinMain()
    f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (547): TestMemLeak.exe!__tmainCRTStartup() + 0x2C bytes
    f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (371): TestMemLeak.exe!wWinMainCRTStartup()
    KERNEL32.DLL!BaseThreadInitThunk() + 0x19 bytes
    ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0x11E bytes
    ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xEE bytes


  Data:
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........


Visual Leak Detector detected 1 memory leak (2036 bytes).
Largest number used: 14610 bytes.
Total allocations: 16326 bytes.
Visual Leak Detector is now exiting.

从上面生成的报告信息可以看出,发生内存泄漏的代码文件testmemleak.cpp及行号(70) ,能看到详细的函数调用堆栈,还能看到发生泄漏的内存中的数据。一般通过这些信息,我们可以快速地定位问题。

3、总结

       在使用上,Visual Leak Detector简单方便,结果报告一目了然。在原理上,Visual Leak Detector针对内存泄漏问题的特点,可谓对症下药——内存泄漏不是不容易发现吗?那就每次内存分配是都给记录下来,程序退出时算总账;内存泄漏现象出现时不是已时过境迁,并非当时泄漏点的现场了吗?那就把现场也记录下来,清清楚楚的告诉使用者那块泄漏的内存就是在如何一个调用过程中泄漏掉的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/100086.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每日leetcode_2441_对应负数同时存在的最大整数

Leetcode每日一题_2441_对应负数同时存在的最大整数 记录自己的成长&#xff0c;加油。 题目 解题 class Solution {public int findMaxK(int[] nums) {int k -1;Set<Integer> set new HashSet<Integer>();for (int x : nums) {set.add(x);}for (int x : nums) …

Spark 9:Spark 新特性

Spark 3.0 新特性 Adaptive Query Execution 自适应查询(SparkSQL) 由于缺乏或者不准确的数据统计信息(元数据)和对成本的错误估算(执行计划调度)导致生成的初始执行计划不理想&#xff0c;在Spark3.x版本提供Adaptive Query Execution自适应查询技术&#xff0c;通过在”运行…

通过位运算,实现单字段标识多个状态位

可能经常有如下这种需求: 需要一张表,来记录学员课程的通过与否. 课程数量不确定,往往很多,且会有变动,随时可能新增一门课. 这种情况下,在设计表结构时,一门课对应一个字段,就有些不合适, 因为不知道课程的具体数量,也无法应对后期课程的增加. 考虑只用一个状态标志位,利用位运…

C/C++实现简单高并发http服务器

基础知识 html&#xff0c;全称为html markup language&#xff0c;超文本标记语言。 http&#xff0c;全称hyper text transfer protocol&#xff0c;超文本传输协议。用于从万维网&#xff08;WWW&#xff1a;World Wide Web&#xff09;服务器传输超文本到本地浏览器的传送…

6-3 递增的整数序列链表的插入 分数 5

List Insert(List L, ElementType X) {//创建结点List node (List)malloc(sizeof(List));node->Data X;node->Next NULL;List head L->Next; //定位real头指针//空链表 直接插入if (head NULL) {L->Next node;node->Next head;return L;}//插入数据比第…

嵌入式养成计划-38----C++--匿名对象--友元--常成员函数和常对象--运算符重载

八十七、匿名对象 概念&#xff1a;没有名字对象格式 &#xff1a;类名&#xff08;&#xff09;;作用 用匿名对象给有名对象初始化的用匿名对象给对象数组初始化的匿名对象作为函数实参使用 示例 : #include <iostream> using namespace std; class Dog { private:s…

在Kubernetes中实现gRPC流量负载均衡

在尝试将gRPC服务部署到Kubernetes集群中时&#xff0c;一些用户&#xff08;包括我&#xff09;面临的挑战之一是实现适当的负载均衡。在深入了解如何平衡gRPC的方式之前&#xff0c;我们首先需要回答一个问题&#xff0c;即为什么需要平衡流量&#xff0c;如果Kubernetes已经…

亘古难题——前端开发or后端开发

一、引言 前端开发 前端开发是创建WEB页面或APP等前端界面呈现给用户的过程&#xff0c;通过HTML&#xff0c;CSS及JavaScript以及衍生出来的各种技术、框架、解决方案&#xff0c;来实现互联网产品的用户界面交互。 前端开发从网页制作演变而来&#xff0c;名称上有很明显的时…

C语言-数组

C 语言支持数组数据结构&#xff0c;数组是一个由若干相同类型变量组成的有序集合。 这里的有序是指数组元素在内存中的存放方式是有序的&#xff0c;即所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素&#xff0c;最高的地址对应最后一个元素。 在 C 语言中&am…

GNU和Linux的关系、 Linux的发行版本、CentOs和RedHat的区别

GNU和Linux的关系 其实&#xff0c;我们通常称之为的"Linux"系统&#xff0c;相对更准确的名称应该称为“GNU/Linux”系统&#xff01; 一个功能完全的操作系统需要许多不同的组成部分&#xff0c;其中就包括内核及其他组件&#xff1b;而在GNU/Linux系统中的内核就…

物联网AI MicroPython传感器学习 之 MQ136硫化氢传感器

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; 一、产品简介 MQ136 是一种硫化氢检测传感器&#xff0c;感应范围为 1 - 200ppm。传感元件是 SnO2&#xff0c;它在清洁空气中的电导率较低。当存在 H₂S 气体时&#xff0c;传感器的电导率随着气体浓度的升…

LeetCode 24.两两交换链表中的结点

题目链接 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目解析 首先可以特判一下&#xff0c;如果结点数目小于等于1&#xff0c;则直接返回即可&#xff0c;因为数目小于等于1就不需要交换了。 然后我们可以创建一个虚拟的头结点&#xff0c;然…

【力扣2011】执行操作后的变量值

&#x1f451;专栏内容&#xff1a;力扣刷题⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、题目描述二、题目分析 一、题目描述 题目链接&#xff1a;执行操作后的变量值 存在一种仅支持 4 种操作和 1 个变量 …

用IDEA操作数据库--MySQL

IDEA集成了DataGrip的操作数据库的功能 就可以省略我们下载SQLyog/Navicat/DataGrip这些图形化操作工具了 以下是IDEA的使用 输入数据库的用户和密码

陀螺仪传感器解读-Gyro Acce,1

加速度计和陀螺仪的简介 https://www.cnblogs.com/zdxgloomy/articles/4171937.html 加速度计和陀螺仪的使用指南 &#xff0c;代码部分 https://www.amobbs.com/forum.php?modviewthread&tid5510930&_dsign972b156c 模拟加速度计: 1. Accelerometer prinicple. 加…

Java基本数据类型

Java基本数据类型 1 数值型 整型数据类型 数据类型内存空间&#xff08;8位1字节&#xff09;取值范围byte(字节型&#xff09;8位&#xff08;1字节&#xff09;-128~127 &#xff08;2的8次方&#xff09;short(短整型&#xff09;16位&#xff08;2字节&#xff09;-32768~3…

算法练习11——买卖股票的最佳时机 II

122. 买卖股票的最佳时机 II 给你一个整数数组 prices &#xff0c;其中 prices[i] 表示某支股票第 i 天的价格。 在每一天&#xff0c;你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买&#xff0c;然后在 同一天 出售。 返回 你能获得…

运维知识点汇总

一.公共基础 linux常用目录 链接一 链接二 linux系统启动 链接一 链接二 LVM 链接一 磁盘挂载 链接一 文件权限 链接一 二.VLAN详解 链接 三.中间件 单体部署&#xff1a; 优点&#xff1a; &#xff08;1&#xff09;小团队成型即可完成开发-测试-上线&am…

简单实现接口自动化测试(基于python+unittest)

简介 本文通过从Postman获取基本的接口测试Code简单的接口测试入手&#xff0c;一步步调整优化接口调用&#xff0c;以及增加基本的结果判断&#xff0c;讲解Python自带的Unittest框架调用&#xff0c;期望各位可以通过本文对接口自动化测试有一个大致的了解。 引言 为什么要…

软件行业与就业(导师主讲)

在企业软件应用的整体架构体系中&#xff0c;有一部分被称为中间件&#xff0c;那么什么叫中间件&#xff1f; 中间件&#xff08;Middleware&#xff09;是指位于操作系统和应用程序之间的一层软件层&#xff0c;它提供了一组工具和服务&#xff0c;用于简化和增强企业软件应用…