【Visual Leak Detector】核心原始碼剖析(VLD 1.0)

2023-04-28 06:00:36

說明

使用 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 的原始碼進行剖析。

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

其中 3.cpp 檔案,4.h 檔案,2.txt 檔案,1.dsp 檔案,1.html 檔案,各檔案用途簡述如下:

  • 檔案 README.html 為網頁版的幫助檔案,裡面介紹了 VLD 的功能、使用方法、設定選項、編譯方法、功能限制等。從這個幫助檔案中可以得知:這個版本的 VLD 只能檢測由 newmalloc 導致的記憶體漏失;若需要檢測多個 DLL 庫,則要確保載入這些庫前,已經包含了 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);
}