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的内存检测原理及实现,感兴趣的朋友可以来了解一下。

Visual Leak Detector内存泄漏检测机制源码剖析_第1张图片

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

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

Visual Leak Detector内存泄漏检测机制源码剖析_第2张图片

       关于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针对内存泄漏问题的特点,可谓对症下药——内存泄漏不是不容易发现吗?那就每次内存分配是都给记录下来,程序退出时算总账;内存泄漏现象出现时不是已时过境迁,并非当时泄漏点的现场了吗?那就把现场也记录下来,清清楚楚的告诉使用者那块泄漏的内存就是在如何一个调用过程中泄漏掉的。

你可能感兴趣的:(C/C++技术分享,VLD,内存泄漏检测,源码剖析,堆内存,堆管理)