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编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

Java的IO模型、Netty原理解析

《Java的IO模型、Netty原理解析》Java的I/O是以流的方式进行数据输入输出的,Java的类库涉及很多领域的IO内容:标准的输入输出,文件的操作、网络上的数据传输流、字符串流、对象流等,这篇... 目录1.什么是IO2.同步与异步、阻塞与非阻塞3.三种IO模型BIO(blocking I/O)NI

JAVA封装多线程实现的方式及原理

《JAVA封装多线程实现的方式及原理》:本文主要介绍Java中封装多线程的原理和常见方式,通过封装可以简化多线程的使用,提高安全性,并增强代码的可维护性和可扩展性,需要的朋友可以参考下... 目录前言一、封装的目标二、常见的封装方式及原理总结前言在 Java 中,封装多线程的原理主要围绕着将多线程相关的操

kotlin中的模块化结构组件及工作原理

《kotlin中的模块化结构组件及工作原理》本文介绍了Kotlin中模块化结构组件,包括ViewModel、LiveData、Room和Navigation的工作原理和基础使用,本文通过实例代码给大家... 目录ViewModel 工作原理LiveData 工作原理Room 工作原理Navigation 工

Java的volatile和sychronized底层实现原理解析

《Java的volatile和sychronized底层实现原理解析》文章详细介绍了Java中的synchronized和volatile关键字的底层实现原理,包括字节码层面、JVM层面的实现细节,以... 目录1. 概览2. Synchronized2.1 字节码层面2.2 JVM层面2.2.1 ente

MySQL的隐式锁(Implicit Lock)原理实现

《MySQL的隐式锁(ImplicitLock)原理实现》MySQL的InnoDB存储引擎中隐式锁是一种自动管理的锁,用于保证事务在行级别操作时的数据一致性和安全性,本文主要介绍了MySQL的隐式锁... 目录1. 背景:什么是隐式锁?2. 隐式锁的工作原理3. 隐式锁的类型4. 隐式锁的实现与源代码分析4

MySQL中Next-Key Lock底层原理实现

《MySQL中Next-KeyLock底层原理实现》Next-KeyLock是MySQLInnoDB存储引擎中的一种锁机制,结合记录锁和间隙锁,用于高效并发控制并避免幻读,本文主要介绍了MySQL中... 目录一、Next-Key Lock 的定义与作用二、底层原理三、源代码解析四、总结Next-Key L

Spring Cloud Hystrix原理与注意事项小结

《SpringCloudHystrix原理与注意事项小结》本文介绍了Hystrix的基本概念、工作原理以及其在实际开发中的应用方式,通过对Hystrix的深入学习,开发者可以在分布式系统中实现精细... 目录一、Spring Cloud Hystrix概述和设计目标(一)Spring Cloud Hystr

MySQL中的MVCC底层原理解读

《MySQL中的MVCC底层原理解读》本文详细介绍了MySQL中的多版本并发控制(MVCC)机制,包括版本链、ReadView以及在不同事务隔离级别下MVCC的工作原理,通过一个具体的示例演示了在可重... 目录简介ReadView版本链演示过程总结简介MVCC(Multi-Version Concurr