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

相关文章

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

MyBatis-Plus 与 Spring Boot 集成原理实战示例

《MyBatis-Plus与SpringBoot集成原理实战示例》MyBatis-Plus通过自动配置与核心组件集成SpringBoot实现零配置,提供分页、逻辑删除等插件化功能,增强MyBa... 目录 一、MyBATis-Plus 简介 二、集成方式(Spring Boot)1. 引入依赖 三、核心机制

redis和redission分布式锁原理及区别说明

《redis和redission分布式锁原理及区别说明》文章对比了synchronized、乐观锁、Redis分布式锁及Redission锁的原理与区别,指出在集群环境下synchronized失效,... 目录Redis和redission分布式锁原理及区别1、有的同伴想到了synchronized关键字

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、

setsid 命令工作原理和使用案例介绍

《setsid命令工作原理和使用案例介绍》setsid命令在Linux中创建独立会话,使进程脱离终端运行,适用于守护进程和后台任务,通过重定向输出和确保权限,可有效管理长时间运行的进程,本文给大家介... 目录setsid 命令介绍和使用案例基本介绍基本语法主要特点命令参数使用案例1. 在后台运行命令2.

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、