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

news2024/7/4 4:32:59

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 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()。

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.coloradmin.cn/o/1076982.html

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

相关文章

10分钟深入探讨带你彻底理解浅拷贝与深拷贝

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 &#x1f4d8; 引言 &#x1f4d8; 1. 深拷贝…

阿里春招JAVA后端面试总结

阿里巴巴春招的后端面经,问了比较多的计算机基础和数据库的内容。 操作系统 一个操作系统,我们在衡量它的内存占用的时候,它一般会有哪些内存的部分? 答:堆和栈 补充: 这个其实是问你对free命令的理解。 主机的内存做一些清理的动作。你知道这里面会涉及到对哪些…

三、监控搭建-Prometheus-grafana部署

三、监控搭建-Prometheus-grafana部署 1、背景2、目标3、传承4、操作 1、背景 在前两篇中介绍了部署prometheus平台和主机采集端部署&#xff0c;都是采用的单查询信息检索&#xff0c;不是太直观 2、目标 实现可视化查看 3、传承 本篇操作依赖[《监控搭建-Prometheus》 和…

Pymol做B因子图

分子动力学模拟结束后&#xff0c;获得蛋白的平均结构&#xff0c; 比如获得的平均结构为WT-average.pdb 然后将平均结构导入到Pymol 中&#xff0c;可以得到B因子图。 gmx rmsf -f md_0_100_noPBC.xtc -s md_0_100.tpr -o rmsf-per-residue.xvg -ox average.pdb -oq bfactors…

猫头虎博主第六期赠书活动:《手机摄影短视频和后期从小白到高手》

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

iOS 17又爆WiFi连接定期断开问题,可尝试这几种解决办法!

从刮痕、掉漆、镜头进灰&#xff0c;再到机型过热烧屏&#xff0c;这届苹果iPhone 15存在的问题还真不少&#xff01; 近日又有用户反馈称他们在 iOS 17 上遇到了 Wi-Fi 问题&#xff0c;这一问题在 iPhone 15系列机型上尤其严重。具体表现在Wi-Fi 在 iPhone 15 Pro 上会定期断…

腾讯云 轻量云 上海 VPS 测评

description: 发布于 2023-07-05腾讯云 轻量云 上海 VPS 测评 腾讯云国内机非常稳定&#xff0c;一年用下来没有掉线丢包的情况。国内机适合与备案域名一起建站使用。带宽很小&#xff0c;图片资源使用CDN加速或海外机提供。 规格 CPU - 2核 内存 - 2GB 系统盘 - SSD云硬盘…

Linux:redis数据库源码包安装

介绍 1.关系数据库与非关系型数据库 1.1关系型数据库 1)一个结构化的数据库&#xff0c;创建在关系模型基础上&#xff0c;一般面向于记录 2)包括Oracle、MySQL、SQL Server、Microsoft Access、DB2等 非关系型数据库 1)除了主流的关系型数据库以外的数据库&#xff0c;都认为是…

PTA 7-6 盲盒包装流水线(单调栈)

题目 众所周知&#xff0c;PAT 有 9 枚徽章&#xff0c;分别对应青铜、白银、黄金、白金、钻石、大师、王者、大圣、天神这 9 个段位&#xff0c;只有成绩非常优秀的考生才有资格获得刻有自己名字的徽章。现在&#xff0c;PAT 制作了徽章的小型纪念版&#xff0c;要制成盲盒给…

SpringbootWeb快速入门

1. 创建新项目&#xff0c;并勾选相关依赖 选中Spring Initializr&#xff0c;设置相关项 点击next选中spring web 点击create 2. 定义HelloController类&#xff0c;添加方法和注解 import org.springframework.web.bind.annotation.RequestMapping;: 这一行导入了Spring MVC…

实际开发中常用的Git操作

文章目录 前言基础知识集中式版本控制 - SVN分布式版本控制 - Git常用的Linux命令Git工作区域 Git 常用命令获取Git仓库添加/提交/推送/删除/回退文件查看信息Git分支Git标签Gitk&#xff1a;一个排查Git问题的工具 前言 git是用C语言开发的&#xff0c;以追求最高的性能。git…

GEO生信数据挖掘(六)实践案例——四分类结核病基因数据预处理分析

前面五节&#xff0c;我们使用阿尔兹海默症数据做了一个数据预处理案例&#xff0c;包括如下内容&#xff1a; GEO生信数据挖掘&#xff08;一&#xff09;数据集下载和初步观察 GEO生信数据挖掘&#xff08;二&#xff09;下载基因芯片平台文件及注释 GEO生信数据挖掘&…

香港服务器在大陆连不上怎么回事?

众所周知&#xff0c;香港服务器与中国内地的网络连通性是比较好的&#xff0c;不仅是机房地理距离的加持&#xff0c;还有就是利用CN2 GIABGP高速线路&#xff0c;参考恒创科技香港服务器访问内地网站&#xff0c;无需绕国际线路转换再到大陆&#xff0c;访问速度会比较快。但…

几行cmd命令,轻松将java文件打包成jar文件

1. 在任意目录下建立一个.java文件 2. 在当前目录下使用cmd命令&#xff1a; javac filename编译 如果报错则使用此命令javac -encoding UTF-8 filename 3.此时已成功生成.class文件 4. 可以手动添加MANIFEST.MF文件 Manifest-Version: 1.0 Main-Class: fileName 5.直接一…

实施运维01

一.运维实施工程师所具备的知识 1.运维工程师&#xff0c;实施工程师是啥&#xff1f; 运维工程师负责服务的稳定性&#xff0c;确保服务无间断的为客户提供服务. 实施工程师负责工程的实施工作&#xff0c;负责现场培训&#xff0c;一般都要出差&#xff0c;哪里有项目就去…

掌动智能:性能测试工具优势有哪些

由于应用程序的性能直接影响用户体验和满意度。而性能问题可能会导致应用响应缓慢、崩溃或无法处理大量用户请求。为了确保应用程序的高性能和可靠性&#xff0c;开发团队需要对应用程序进行性能测试。性能测试工具能够模拟真实场景下的负载并监测应用程序的性能表现&#xff0…

openGauss学习笔记-95 openGauss 数据库管理-访问外部数据库-postgres_fdw

文章目录 openGauss学习笔记-95 openGauss 数据库管理-访问外部数据库-postgres_fdw95.1 使用postgres_fdw95.2 postgres_fdw下推主要成分95.3 常见问题95.4 注意事项 openGauss学习笔记-95 openGauss 数据库管理-访问外部数据库-postgres_fdw openGauss的fdw实现的功能是各个…

基于SSM的医用物理学实验考核系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

IDEA的使用(二)快捷键 (IntelliJ IDEA 2022.1.3版本)

1. IDEA中的常用快捷键 1.1 通用型快捷键 1.2 提高编写速度 ctrl shift ↑或↓ 只能在方法里面移动代码。 alt shift ↑或↓ 可以向方法外移动代码。 设置过自动导包&#xff0c;所以不用批量导包啦。 1.3 类结构、查找和查看源码 1.4 查找、替换和关闭 1.5 调整格式 1.6 De…

WebSocket协议:实现实时双向通信的秘诀

目录 &#x1f407;今日良言&#xff1a;海压竹枝低复举&#xff0c;风吹山角晦还明 &#x1f407;一、WebSocket协议介绍 &#x1f407;二、WebSocket如何使用 &#x1f407;三、WebSocket和HTTP的区别 &#x1f407;今日良言&#xff1a;海压竹枝低复举&#xff0c;风吹山…