【Visual Leak Detector】核心源码剖析(VLD 1.0)

说明

使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。本篇对 VLD 1.0 源码做内存泄漏检测的思路进行剖析。同系列文章目录可见 《内存泄漏检测工具》目录


1. 源码获取

version 1.0 及之前版本都使用旧的检测思路,可以在网站 CodeProject-Visual-Leak-Detector 中下载 version 1.0 的源码(国内网络资源:百度网盘-vld-1.0 源码包),同时在该网站中可以看到库作者 Dan Moulding 对旧检测原理的介绍。这个网站中有下图这段文字,但经过我一番查找,还是未找到 Dan Moulding 对后续新检测原理的介绍文章,本篇文章主要对 version 1.0 的源码进行剖析。

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

version 1.0 的源码算上注释一共不到 3000 行,而且代码注释写得很详细,推荐有兴趣的仔细阅读源码。以下资料可能对理解其检测原理有帮助:

2. 源码文件概览

version 1.0 源码包中一共有 11 个文件,目录结构如下:

vld-10-src
    CHANGES.txt
    COPYING.txt
    README.html
    vld.cpp
    vld.dsp
    vld.h
    vldapi.cpp
    vldapi.h
    vldint.h
    vldutil.cpp
    vldutil.h

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

其中 3.cpp 文件,4.h 文件,2.txt 文件,1.dsp 文件,1.html 文件,各文件用途简述如下:

  • 文件 README.html 为网页版的帮助文档,里面介绍了 VLD 的功能、使用方法、配置选项、编译方法、功能限制等。从这个帮助文档中可以得知:这个版本的 VLD 只能检测由 newmalloc 导致的内存泄漏,无法检测 COM 泄漏、进程外的泄漏、与 CRT 堆无关的泄漏;当需要检测多个 DLL 库时,则要在加载的第一个模块中包含 vld.h 头文件(若第一个加载的模块是个某个 DLL ,则在该 DLL 的主源文件中包含 vld.h,若第一个加载的是主 exe,则在 main 函数所在文件中包含 vld.h)。

  • 文件 CHANGES.txt 为版本迭代日志,记录了各版本的更新概要;

  • 文件 COPYING.txtLGPL 2.1 开源协议;

  • 文件 vld.dspVisual C++ 的项目文件,全称 Microsoft Developer Studio Project File

  • 文件 vldapi.h 为使用 VLD 库时需包含的头文件之一,里面声明了两个接口:VLDEnable()VLDDisable()

  • 文件 vldapi.cpp 里面是接口 VLDEnable()VLDDisable() 的函数定义;

  • 文件 vldint.h 里面定义了 dbghelp.dll 中一些函数的别名,并声明了 VisualLeakDetector 类;

  • 文件 vldutil.h 里面定义了一些 VLD 内部使用的宏,重载了内部的 new/delete 运算符,并声明了 CallStack 类与 BlockMap 类,这两个类是 VLD 自定义的数据结构,用来存储泄漏信息,CallStack 类似于 STL vectorBlockMap 类似于 STL map

  • 文件 vldutil.cppCallStackBlockMap 的类方法实现;

  • 文件 vld.h 为使用 VLD 库时需包含的头文件之一,里面是一些配置选项的宏定义,用户可使用这些宏来定制 VLD 的功能。特别地,这个文件里有以下一行代码,用来强制引用 VLD 库中的全局对象 visualleakdetector,使其链接到当前程序(资料参考 MSDN-pragma-commentMSDN-Linker-optionsMSDN-/INCLUDE)。

    // Force a symbolic reference to the global VisualLeakDetector class object from
    // the library. This enusres that the object is linked with the program, even
    // though nobody directly references it outside of the library.
    #pragma comment(linker, "/include:?visualleakdetector@@3VVisualLeakDetector@@A")
    
  • 文件 vld.cppVisualLeakDetector 的类方法实现,主要功能的代码都在这个文件里;

3. 源码剖析

3.1 注册自定义 AllocHook 函数

使用 #pragma init_seg (compiler) 指令构造一个全局对象 visualleakdetector,来确保这个对象的构造函数最先被调用(详见 vld.cpp 第 49~55 行)。

// The one and only VisualLeakDetector object instance. This is placed in the
// "compiler" initialization area, so that it gets constructed during C runtime
// initialization and before any user global objects are constructed. Also,
// disable the warning about us using the "compiler" initialization area.
#pragma warning (disable:4074)
#pragma init_seg (compiler)
VisualLeakDetector visualleakdetector;

在全局对象 visualleakdetector 的构造函数中调用 _CrtSetAllocHook 接口注册自定义 AllocHook 函数,使程序能捕捉之后的内存操作(内存分配/内存释放)事件(详见 vld.cpp 第 57~95 行)。

// Constructor - Dynamically links with the Debug Help Library and installs the
//   allocation hook function so that the C runtime's debug heap manager will
//   call the hook function for every heap request.
//
VisualLeakDetector::VisualLeakDetector ()
{
    ...

    if (m_tlsindex == TLS_OUT_OF_INDEXES) {
        report("ERROR: Visual Leak Detector: Couldn't allocate thread local storage.\n");
    }
    else if (linkdebughelplibrary()) {
        // Register our allocation hook function with the debug heap.
        m_poldhook = _CrtSetAllocHook(allochook);
        report("Visual Leak Detector Version "VLD_VERSION" installed ("VLD_LIBTYPE").\n");
        ...
    }
    
    report("Visual Leak Detector is NOT installed!\n");
}

此外,在 visualleakdetector 的构造函数中,还做了以下工作:

  • 初始化 VLD 的配置信息,详见 vld.cpp 第 71~75 行、第 84~90 行、以及 reportconfig() 函数,详见 vld.cpp 第 768~800 行。
  • 动态加载 dbghelp.dll 库,用于后续获取调用堆栈信息,详见 linkdebughelplibrary() 函数,vld.cpp 第 662~741 行,所使用的 dbghelp.dll 库版本为 6.3.17.0

3.2 使用 StackWalk64 获取调用堆栈信息

全局对象 visualleakdetector 有一个成员变量 m_mallocmap,用来存储堆内存分配时的调用堆栈信息,这是一种基于红黑树的自定义 Map 容器(类似于 STLmap),这个容器的声明及定义可见 vldutil.hvldutil.cpp 文件 。

////////////////////////////////////////////////////////////////////////////////
//
//  The BlockMap Class
//
//  This data structure is similar in concept to a STL map, but is specifically
//  tailored for use by VLD, making it more efficient than a standard STL map.
//
//  The purpose of the BlockMap is to map allocated memory blocks (via their
//  unique allocation request numbers) to the call stacks that allocated them.
//  One of the primary concerns of the BlockMap is to be able to quickly insert
//  search and delete. For this reason, the underlying data structure is
//  a red-black tree (a type of balanced binary tree).
//
//  The red-black tree is overlayed on top of larger "chunks" of pre-allocated
//  storage. These chunks, which are arranged in a linked list, make it possible
//  for the map to have reserve capacity, allowing it to grow dynamically
//  without incurring a heap hit each time a new element is added to the map.
//
class BlockMap
{
    ...
};

每次进行内存操作(alloc/realloc/free)时,都会自动执行前述自定义的 AllocHook 函数,其定义如下,详见 vld.cpp 第 175~260 行。

// allochook - This is a hook function that is installed into Microsoft's
//   CRT debug heap when the VisualLeakDetector object is constructed. Any time
//   an allocation, reallocation, or free is made from/to the debug heap,
//   the CRT will call into this hook function.
//
//  Note: The debug heap serializes calls to this function (i.e. the debug heap
//    is locked prior to calling this function). So we don't need to worry about
//    thread safety -- it's already taken care of for us.
//
//  - type (IN): Specifies the type of request (alloc, realloc, or free).
//
//  - pdata (IN): On a free allocation request, contains a pointer to the
//      user data section of the memory block being freed. On alloc requests,
//      this pointer will be NULL because no block has actually been allocated
//      yet.
//
//  - size (IN): Specifies the size (either real or requested) of the user
//      data section of the memory block being freed or requested. This function
//      ignores this value.
//
//  - use (IN): Specifies the "use" type of the block. This can indicate the
//      purpose of the block being requested. It can be for internal use by
//      the CRT, it can be an application defined "client" block, or it can
//      simply be a normal block. Client blocks are just normal blocks that
//      have been specifically tagged by the application so that the application
//      can separately keep track of the tagged blocks for debugging purposes.
//
//  - request (IN): Specifies the allocation request number. This is basically
//      a sequence number that is incremented for each allocation request. It
//      is used to uniquely identify each allocation.
//
//  - filename (IN): String containing the filename of the source line that
//      initiated this request. This function ignores this value.
//
//  - line (IN): Line number within the source file that initiated this request.
//      This function ignores this value.
//
//  Return Value:
//
//    Always returns true, unless another allocation hook function was already
//    installed before our hook function was called, in which case we'll return
//    whatever value the other hook function returns. Returning false will
//    cause the debug heap to deny the pending allocation request (this can be
//    useful for simulating out of memory conditions, but Visual Leak Detector
//    has no need to make use of this capability).
//
int VisualLeakDetector::allochook (int type, void *pdata, size_t size, int use, long request, const unsigned char *file, int line)
{
    ...

    // Call the appropriate handler for the type of operation.
    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;
    }

    ...
}

这个函数的输入参数中,有一个 request 值,这个值被用来做为所分配内存块的唯一标识符,即 m_mallocmapkey 值。函数体中,会根据内存操作事件的类型做对应的处理,hookmalloc()hookfree()hookrealloc() 的定义详见 vld.cpp 第 594~660 行。

void VisualLeakDetector::hookfree (const void *pdata)
{
    long request = pHdr(pdata)->lRequest;

    m_mallocmap->erase(request);
}

void VisualLeakDetector::hookmalloc (long request)
{
    CallStack *callstack;

    if (!enabled()) {
        // Memory leak detection is disabled. Don't track allocations.
        return;
    }

    callstack = m_mallocmap->insert(request);
    getstacktrace(callstack);
}

void VisualLeakDetector::hookrealloc (const void *pdata, long request)
{
    // Do a free, then do a malloc.
    hookfree(pdata);
    hookmalloc(request);
}

(1)若涉及到分配新内存,则使用内联汇编技术获取当前程序地址,然后将其作为参数初值,循环调用 StackWalk64 接口获得完整的调用堆栈信息 CallStack(调用堆栈中各指令的地址信息),详见 getstacktrace() 函数,vld.cpp 第 530~592 行,接着与 request 值关联一起插入到 m_mallocmap 中。如下所示,其中的 pStackWalk64 是一个函数指针,指向 dbghelp.dll 库中的 StackWalk64 函数。

void VisualLeakDetector::getstacktrace (CallStack *callstack)
{
    DWORD        architecture;
    CONTEXT      context;
    unsigned int count = 0;
    STACKFRAME64 frame;
    DWORD_PTR    framepointer;
    DWORD_PTR    programcounter;

    // Get the required values for initialization of the STACKFRAME64 structure
    // to be passed to StackWalk64(). Required fields are AddrPC and AddrFrame.
#if defined(_M_IX86) || defined(_M_X64)
    architecture = X86X64ARCHITECTURE;
    programcounter = getprogramcounterx86x64();
    __asm mov [framepointer], BPREG // Get the frame pointer (aka base pointer)
#else
// If you want to retarget Visual Leak Detector to another processor
// architecture then you'll need to provide architecture-specific code to
// retrieve the current frame pointer and program counter in order to initialize
// the STACKFRAME64 structure below.
#error "Visual Leak Detector is not supported on this architecture."
#endif // defined(_M_IX86) || defined(_M_X64)

    // Initialize the STACKFRAME64 structure.
    memset(&frame, 0x0, sizeof(frame));
    frame.AddrPC.Offset    = programcounter;
    frame.AddrPC.Mode      = AddrModeFlat;
    frame.AddrFrame.Offset = framepointer;
    frame.AddrFrame.Mode   = AddrModeFlat;

    // 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);
    }
}

通过内联汇编获取当前程序地址的代码详见 getprogramcounterx86x64() 函数,vld.cpp 第 501~528 行,如下,通过 return 这个函数的返回地址得到。

// getprogramcounterx86x64 - Helper function that retrieves the program counter
//   (aka the EIP (x86) or RIP (x64) register) for getstacktrace() on Intel x86
//   or x64 architectures (x64 supports both AMD64 and Intel EM64T). There is no
//   way for software to directly read the EIP/RIP register. But it's value can
//   be obtained by calling into a function (in our case, this function) and
//   then retrieving the return address, which will be the program counter from
//   where the function was called.
//
//  Note: Inlining of this function must be disabled. The whole purpose of this
//    function's existence depends upon it being a *called* function.
//
//  Return Value:
//
//    Returns the caller's program address.
//
#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)

(2)若涉及到释放旧内存,则从 m_mallocmap 中去除这个内存块对应的 request 值及 CallStack 信息,详见 hookfree() 函数。

3.3 遍历双向链表生成泄漏检测报告

程序结束时,全局对象 visualleakdetector 的析构函数最后被调用(因为构造顺序与析构顺序相反)。在它的析构函数中(详见 vld.cpp 第 97~173 行),主要做了以下几件事:

(1)注销自定义 AllocHook 函数。

// Unregister the hook function.
pprevhook = _CrtSetAllocHook(m_poldhook);
if (pprevhook != allochook) {
    // WTF? Somebody replaced our hook before we were done. Put theirs
    // back, but notify the human about the situation.
    _CrtSetAllocHook(pprevhook);
    report("WARNING: Visual Leak Detector: The CRT allocation hook function was unhooked prematurely!\n"
           "    There's a good possibility that any potential leaks have gone undetected!\n");
}

(2)生成泄漏检测报告。详见 reportleaks() 函数,vld.cpp 第 802~962 行。报告生成思路如下:

  • Debug 模式下,每次分配内存时,系统都会给分配的数据块加上一个内存管理头 _CrtMemBlockHeader,如下所示,这个结构体有 pBlockHeaderNextpBlockHeaderPrev 两个成员变量,通过它们可以访问到其他已分配的内存块,全部的内存管理头组合在一起形成了一个双向链表结构,而新加入的内存管理头会被放置在该链表的头部。当释放内存时,对应的节点会在链表中被剔除。

    typedef struct _CrtMemBlockHeader {
        struct _CrtMemBlockHeader* pBlockHeaderNext;
        struct _CrtMemBlockHeader* pBlockHeaderPrev;
        char*                       szFileName;
        int                         nLine;
    #ifdef _WIN64
        /* These items are reversed on Win64 to eliminate gaps in the struct
         * and ensure that sizeof(struct)%16 == 0, so 16-byte alignment is
         * maintained in the debug heap.
         */
        int                         nBlockUse;
        size_t                      nDataSize;
    #else  /* _WIN64 */
        size_t                      nDataSize;
        int                         nBlockUse;
    #endif  /* _WIN64 */
        long                        lRequest;
        unsigned char               gap[nNoMansLandSize];
        /* followed by:
         *  unsigned char           data[nDataSize];
         *  unsigned char           anotherGap[nNoMansLandSize];
         */
    } _CrtMemBlockHeader;
    

    因此只需要临时 new 一块内存,就可以根据这个临时内存块的地址推导出链表头指针,如下代码中的 pheader。然后遍历这个链表,可以得到程序快结束时,仍未释放的内存信息。

    pheap = new char;
    pheader = pHdr(pheap)->pBlockHeaderNext;
    delete pheap;
    
  • 遍历过程中,依据每个节点的 nBlockUse 值,可以分辨出当前内存块的来由:用户分配、CRT 分配、还是 VLD 分配,据此可做一个筛选,例如只考虑来自用户分配的内存。

    if (_BLOCK_TYPE(pheader->nBlockUse) == _CRT_BLOCK) {
        // Skip internally allocated blocks.
        pheader = pheader->pBlockHeaderNext;
        continue;
    }
    
  • 遍历过程中,在 m_mallocmap 中查找筛选后节点的 lRequest 值,若存在,则表明有内存泄漏发生,由此可以获得发生泄漏的调用堆栈信息 CallStack,这是一系列指令地址。接下来,将这些指令地址作为输入参数,循环调用 SymGetLineFromAddr64 获得源文件名和行数,调用 SymFromAddr 获得函数名。将获取的信息传递给 report() 函数。

    callstack = m_mallocmap->find(pheader->lRequest);
    if (callstack) {
        // Found a block which is still in the allocated list, and which we
        // have an entry for in the allocated block map. We've identified a
        // memory leak.
        if (leaksfound == 0) {
            report("WARNING: Visual Leak Detector detected memory leaks!\n");
        }
        leaksfound++;
        report("---------- Block %ld at "ADDRESSFORMAT": %u bytes ----------\n", pheader->lRequest, pbData(pheader), pheader->nDataSize);
        if (_VLD_configflags & VLD_CONFIG_AGGREGATE_DUPLICATES) {
            // Aggregate all other leaks which are duplicates of this one
            // under this same heading, to cut down on clutter.
            duplicates = eraseduplicates(pheader->pBlockHeaderNext, pheader->nDataSize, callstack);
            if (duplicates) {
                report("A total of %lu leaks match this size and call stack. Showing only the first one.\n", duplicates + 1);
                leaksfound += duplicates;
            }
        }
        report("  Call Stack:\n");
    
        // 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 ((foundline = pSymGetLineFromAddr64(m_process, (*callstack)[frame], &displacement, &sourceinfo)) == TRUE) {
                // Unless the "show useless frames" option has been enabled,
                // don't show frames that are internal to the heap or Visual
                // Leak Detector. There is virtually no situation where they
                // would be useful for finding the source of the leak.
                if (!(_VLD_configflags & VLD_CONFIG_SHOW_USELESS_FRAMES)) {
                    if (strstr(sourceinfo.FileName, "afxmem.cpp") ||
                        strstr(sourceinfo.FileName, "dbgheap.c") ||
                        strstr(sourceinfo.FileName, "new.cpp") ||
                        strstr(sourceinfo.FileName, "vld.cpp")) {
                        continue;
                    }
                }
            }
    
            // 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)";
            }
    
            // Display the current stack frame's information.
            if (foundline) {
                report("    %s (%d): %s\n", sourceinfo.FileName, sourceinfo.LineNumber, functionname);
            }
            else {
                report("    "ADDRESSFORMAT" (File and line number not available): ", (*callstack)[frame]);
                report("%s\n", functionname);
            }
        }
    
        // Dump the data in the user data section of the memory block.
        if (_VLD_maxdatadump != 0) {
            dumpuserdatablock(pheader);
        }
        report("\n");
    }
    
  • report() 函数中格式化后再使用 OutputDebugString 输出泄漏报告。这里用到了 C 语言中的变长参数,用法可参考 博客园-C++ 实现可变参数的三个方法

    // report - Sends a printf-style formatted message to the debugger for display.
    //
    //  - format (IN): Specifies a printf-compliant format string containing the
    //      message to be sent to the debugger.
    //
    //  - ... (IN): Arguments to be formatted using the specified format string.
    //
    //  Return Value:
    //
    //    None.
    //
    void VisualLeakDetector::report (const char *format, ...)
    {
        va_list args;
    #define MAXREPORTMESSAGESIZE 513
        char    message [MAXREPORTMESSAGESIZE];
    
        va_start(args, format);
        _vsnprintf(message, MAXREPORTMESSAGESIZE, format, args);
        va_end(args);
        message[MAXREPORTMESSAGESIZE - 1] = '\0';
    
        OutputDebugString(message);
    }
    

(3)卸载 dbghelp.dll 库。

// Unload the Debug Help Library.
FreeLibrary(m_dbghelp);

(4)泄漏自检。通过遍历系统用于内存管理的双向链表,判断 VLD 自身是否发生内存泄漏,同样是依据每个节点的 nBlockUse 值。

// Do a memory leak self-check.
pheap = new char;
pheader = pHdr(pheap)->pBlockHeaderNext;
delete pheap;
while (pheader) {
    if (_BLOCK_SUBTYPE(pheader->nBlockUse) == VLDINTERNALBLOCK) {
        // Doh! VLD still has an internally allocated block!
        // This won't ever actually happen, right guys?... guys?
        internalleaks++;
        leakfile = pheader->szFileName;
        leakline = pheader->nLine;
        report("ERROR: Visual Leak Detector: Detected a memory leak internal to Visual Leak Detector!!\n");
        report("---------- Block %ld at "ADDRESSFORMAT": %u bytes ----------\n", pheader->lRequest, pbData(pheader), pheader->nDataSize);
        report("%s (%d): Full call stack not available.\n", leakfile, leakline);
        dumpuserdatablock(pheader);
        report("\n");
    }
    pheader = pheader->pBlockHeaderNext;
}
if (_VLD_configflags & VLD_CONFIG_SELF_TEST) {
    if ((internalleaks == 1) && (strcmp(leakfile, m_selftestfile) == 0) && (leakline == m_selftestline)) {
        report("Visual Leak Detector passed the memory leak self-test.\n");
    }
    else {
        report("ERROR: Visual Leak Detector: Failed the memory leak self-test.\n");
    }
}

(5)输出卸载成功的提示信息。这一输出发生在析构函数的结尾括号 } 前。

report("Visual Leak Detector is now exiting.\n");

4. 其他问题

4.1 如何区分分配内存的来由

_CrtMemBlockHeader 结构体有个 nBlockUse 成员变量,用来标识分配用途,这个值是可以人为设置的,VLD 正是利用这一点,重载了 VLD 内部使用的内存分配函数,使得库内部每次进行内存请求时,都会将这个 nBlockUse 设置为 VLD 分配标识,详见 vldutil.h 第 49~153 行。

(1)分配时,核心代码如下,第二个参数为设置的 nBlockUse 值:

void *pdata = _malloc_dbg(size, _CRT_BLOCK | (VLDINTERNALBLOCK << 16), file, line);

(2)使用 nBlockUse 来对分配用途做判断时,核心代码如下:

// 判断是否由 CRT 或 VLD 分配
if (_BLOCK_TYPE(pheader->nBlockUse) == _CRT_BLOCK) {
    ...
}

// 判断是否由 VLD 分配
if (_BLOCK_SUBTYPE(pheader->nBlockUse) == VLDINTERNALBLOCK) {
    ...
}

(3)这里面涉及到的几个宏定义如下:

文件 crtdbg.h 中。

#define _BLOCK_TYPE(block)          (block & 0xFFFF)
#define _BLOCK_SUBTYPE(block)       (block >> 16 & 0xFFFF)

// Memory block identification
#define _FREE_BLOCK      0
#define _NORMAL_BLOCK    1
#define _CRT_BLOCK       2
#define _IGNORE_BLOCK    3
#define _CLIENT_BLOCK    4
#define _MAX_BLOCKS      5

文件 vldutil.h 中。

#define VLDINTERNALBLOCK   0xbf42    // VLD internal memory block subtype

4.2 如何实现多线程检测

使用线程本地存储(Thread Local Storage),参考 MicroSoft-Using-Thread-Local-Storage。全局对象 visualleakdetector 有个成员变量 m_tlsindex,详见 vldint.h 第 146 行,如下:

DWORD m_tlsindex;     // Index for thread-local storage of VLD data

这个变量被用来接收 TlsAlloc() 返回的索引值,在 visualleakdetector 的构造函数中被初始化,详见 vld.cpp 第 69 行、77~79 行,如下:

m_tlsindex = TlsAlloc();

... 
    
if (m_tlsindex == TLS_OUT_OF_INDEXES) {
    report("ERROR: Visual Leak Detector: Couldn't allocate thread local storage.\n");
}

初始化成功后,当前进程的任何线程都可以使用这个索引值来存储和访问对应线程本地的值,不同线程间互不影响,访问获得的结果也与其他线程无关,因此可用它来存储 VLD 在每个线程中的开关状态。在分配新内存时,会触发 hookmalloc() 函数,该函数会在分配行为所属的线程中执行,详见 vld.cpp 第 611~636 行:

void VisualLeakDetector::hookmalloc (long request)
{
    CallStack *callstack;

    if (!enabled()) {
        // Memory leak detection is disabled. Don't track allocations.
        return;
    }

    callstack = m_mallocmap->insert(request);
    getstacktrace(callstack);
}

(1)判断当前线程是否开启了 VLD。在 enabled() 函数中,会调用 TlsGetValue() 访问所属线程本地的值,根据此值判断 VLD 内存检测功能是否处于开启状态。若是第一次访问(此时 TlsGetValue() 的返回值为 VLD_TLS_UNINITIALIZED),则根据用户配置,使用 TlsSetValue() 初始化对应线程本地的值。

// enabled - Determines if memory leak detection is enabled for the current
//   thread.
//
//  Return Value:
//
//    Returns true if Visual Leak Detector is enabled for the current thread.
//    Otherwise, returns false.
//
bool VisualLeakDetector::enabled ()
{
    unsigned long status;

    status = (unsigned long)TlsGetValue(m_tlsindex);
    if (status == VLD_TLS_UNINITIALIZED) {
        // TLS is uninitialized for the current thread. Use the initial state.
        if (_VLD_configflags & VLD_CONFIG_START_DISABLED) {
            status = VLD_TLS_DISABLED;
        }
        else {
            status = VLD_TLS_ENABLED;
        }
        // Initialize TLS for this thread.
        TlsSetValue(m_tlsindex, (LPVOID)status);
    }

    return (status & VLD_TLS_ENABLED) ? true : false;
}

(2)对当前线程设置 VLD 的开关状态。这是两个对外的接口函数,其定义如下,详见 vldapi.cpp 第 31~57 行,使用 TlsSetValue() 设置对应值即可:

void VLDEnable ()
{
    if (visualleakdetector.enabled()) {
        // Already enabled for the current thread.
        return;
    }

    // Enable memory leak detection for the current thread.
    TlsSetValue(visualleakdetector.m_tlsindex, (LPVOID)VLD_TLS_ENABLED);
    visualleakdetector.m_status &= ~VLD_STATUS_NEVER_ENABLED;
}

void VLDDisable ()
{
    if (!visualleakdetector.enabled()) {
        // Already disabled for the current thread.
        return;
    }

    // Disable memory leak detection for the current thread.
    TlsSetValue(visualleakdetector.m_tlsindex, (LPVOID)VLD_TLS_DISABLED);
}
posted @ 2023-04-27 23:14  木三百川  阅读(208)  评论(0编辑  收藏  举报