【Visual Leak Detector】核心源碼剖析(VLD 2.5.1)

来源:https://www.cnblogs.com/young520/archive/2023/05/10/17389224.html
-Advertisement-
Play Games

使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇對 VLD 2.5.1 源碼做記憶體泄漏檢測的思路進行剖析。 ...


說明

使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇對 VLD 2.5.1 源碼做記憶體泄漏檢測的思路進行剖析。同系列文章目錄可見 《記憶體泄漏檢測工具》目錄

目錄


1. 源碼獲取

version 1.0 及之前版本都使用舊的檢測思路:通過 _CrtSetAllocHook 註冊自定義 AllocHook 函數,從而監視程式的記憶體分配事件,詳見本人另一篇博客 核心源碼剖析(VLD 1.0),缺陷是只能檢測由 newmalloc 產生的記憶體泄漏,受限於 _CrtSetAllocHook。從 version 1.9 開始,VLD 換用了新的檢測思路,通過修改導入地址表(Import Address Table)將原先的記憶體操作函數替換為 VLD 自定義的函數,從而可以檢測到更多類型的泄漏。CodeProject-Visual-Leak-Detector百度網盤-vld-1.9d-setup 可以下載 vld 1.9d 的庫及源碼,註意,這個版本的安裝器有個坑,會清空之前的 Path 系統變數,只留下 VLD,需慎用。Github-dmoulding-vld 上有 vld 1.9h 的源碼。Github-KindDragon-vld 上有 vld 2.5.1 的源碼,這是目前的最新版本(其他下載途徑詳見 VLD 2.5.1 源碼下載)。

Oh Shit!-圖片走丟了-打個廣告-歡迎來博客園關註“木三百川”

本篇文章主要對 vld 2.5.1 的源碼進行剖析。以下資料可能對理解其檢測原理有幫助:

2. 源碼文件概覽

以下 26 個文件是 VLD 源碼的核心文件,

vld-master\src
   callstack.cpp
   callstack.h
   criticalsection.h
   crtmfcpatch.h
   dbghelp.h
   dllspatches.cpp
   loaderlock.h
   map.h
   ntapi.cpp
   ntapi.h
   resource.h
   set.h
   stdafx.cpp
   stdafx.h
   tree.h
   utility.cpp
   utility.h
   vld.cpp
   vld.h
   vldallocator.h
   vldapi.cpp
   vldheap.cpp
   vldheap.h
   vldint.h
   vld_def.h
   vld_hooks.cpp

其中有 17.h 文件、9.cpp 文件,各文件用途簡述如下:

  • 以下 5 個文件用於定義 VLD 內部使用的數據結構,set 類似於 STL setmap 類似於 STL maptree 為紅黑樹,callstack 類似於 STL vector

    callstack.cpp
    callstack.h
    map.h
    set.h
    ree.h
    
  • 以下 3 個文件用於定義 VLD 內部使用的記憶體管理函數,供 VLD 內部使用。

    vldallocator.h
    vldheap.cpp
    vldheap.h
    
  • 以下 3 幾個文件用於定義 VLD 修正後的記憶體管理函數,供 VLD 外部使用。進一步跟蹤發現,vld_hooks.cpp 里定義的函數在 VLD 內部也會被調用。

    crtmfcpatch.h
    dllspatches.cpp
    vld_hooks.cpp
    
  • 以下 11 個文件定義了一些通用的函數、變數、巨集等。

    criticalsection.h
    dbghelp.h
    loaderlock.h
    ntapi.cpp
    ntapi.h
    resource.h
    stdafx.cpp
    stdafx.h
    utility.cpp
    utility.h
    vldapi.cpp
    
  • 以下 2 個文件定義了 VisualLeakDetector 類的方法,外部 API 介面的內部實現多在這裡。

    vld.cpp
    vldint.h
    
  • 以下 2 個文件是 VLD 對外的包含文件,裡面聲明瞭 VLDAPI 介面,還有一些配置巨集的定義。

    vld.h
    vld_def.h
    

3. 源碼剖析

vld 2.5.1 自定義了 vld.dll 的入口點函數,核心代碼如下,詳見 vld.cpp 第 76~307 行

#define _DECL_DLLMAIN  // for _CRT_INIT
#include <process.h>   // for _CRT_INIT
#pragma comment(linker, "/entry:DllEntryPoint")

__declspec(noinline)
BOOL WINAPI DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    // Patch/Restore ntdll address that calls the dll entry point
    if (fdwReason == DLL_PROCESS_ATTACH) {
        NtDllPatch((PBYTE)_ReturnAddress(), patch);
    }

    if (fdwReason == DLL_PROCESS_ATTACH || fdwReason == DLL_THREAD_ATTACH)
        if (!_CRT_INIT(hinstDLL, fdwReason, lpReserved))
            return(FALSE);

    if (fdwReason == DLL_PROCESS_DETACH || fdwReason == DLL_THREAD_DETACH)
        if (!_CRT_INIT(hinstDLL, fdwReason, lpReserved))
            return(FALSE);

    if (fdwReason == DLL_PROCESS_DETACH) {
        NtDllRestore(patch);
    }
    return(TRUE);
}

並定義了一個很重要的全局變數,詳見 vld.cpp 第 60~61 行

// The one and only VisualLeakDetector object instance.
__declspec(dllexport) VisualLeakDetector g_vld;

從入口點函數可知:

(1)載入 vld.dll,做了兩件事:先執行 NtDllPatch() 函數、然後執行 VisualLeakDetector 類構造函數(在 _CRT_INIT() 中)。

(2)卸載 vld.dll,也做了兩件事:先執行 VisualLeakDetector 類析構函數(在 _CRT_INIT() 中)、然後執行 NtDllRestore() 函數。

3.1 通過 inline hook 修補 LdrpCallInitRoutine

這是載入 vld.dll 時做的第一件事,在 NtDllPatch() 函數中進行。由於每次載入/卸載 DLL 時,都會進入預設的 LdrpCallInitRoutine() 函數,為了對新載入的 DLL 做記憶體泄漏檢測(如果配置項 ForceIncludeModules 列表包含這個 DLL),VLDNtDllPatch() 函數中使用 inline hook 技術修補了預設的 LdrpCallInitRoutine() 函數,核心代碼如下,詳見 vld.cpp 第 171~258 行

BOOL NtDllPatch(const PBYTE pReturnAddress, NTDLL_LDR_PATCH &NtDllPatch)
{
    if (NtDllPatch.bState == FALSE) {
        ...
        BYTE ptr[] = { 0xFF, 0x75, 0x08 };                                   // push [ebp][08h]
        BYTE mov[] = { 0x90, 0xB8, '?', '?', '?', '?' };                     // mov eax, 0x00000000
        BYTE call[] = { 0xFF, 0xD0 };                                        // call eax
        ...
        BYTE jmp[] = { 0xE9, '?', '?', '?', '?' };                           // jmp 0x00000000
        ...
        if (...) {
            ...
            if (VirtualProtect(NtDllPatch.pDetourAddress, NtDllPatch.nDetourSize, PAGE_EXECUTE_READWRITE, &dwProtect)) {
                memset(NtDllPatch.pDetourAddress, 0x90, NtDllPatch.nDetourSize);
                ...
                
                // Push EntryPoint as last parameter
                memcpy(&NtDllPatch.pDetourAddress[0], &ptr, _countof(ptr));
                
                // Copy original param instructions
                memcpy(&NtDllPatch.pDetourAddress[_countof(ptr)], NtDllPatch.pPatchAddress, nParamSize);
                
                // Move LdrpCallInitRoutine to eax/rax
                *(PSIZE_T)(&mov[2]) = (SIZE_T)LdrpCallInitRoutine;
                memcpy(&NtDllPatch.pDetourAddress[_countof(ptr) + nParamSize], &mov, _countof(mov));

                // Jump to original function
                *(DWORD*)(&jmp[1]) = (DWORD)(pReturnAddress - _countof(call) - (NtDllPatch.pDetourAddress + NtDllPatch.nDetourSize));
                memcpy(&NtDllPatch.pDetourAddress[_countof(ptr) + nParamSize + _countof(mov)], &jmp, _countof(jmp));

                VirtualProtect(NtDllPatch.pDetourAddress, NtDllPatch.nDetourSize, dwProtect, &dwProtect);

                if (VirtualProtect(NtDllPatch.pPatchAddress, NtDllPatch.nPatchSize, PAGE_EXECUTE_READWRITE, &dwProtect)) {
                    memset(NtDllPatch.pPatchAddress, 0x90, NtDllPatch.nPatchSize);

                    // Jump to detour address
                    *(DWORD*)(&jmp[1]) = (DWORD)(NtDllPatch.pDetourAddress - (pReturnAddress - _countof(call)));
                    memcpy(pReturnAddress - _countof(call) - _countof(jmp), &jmp, _countof(jmp));

                    // Call LdrpCallInitRoutine from eax/rax
                    memcpy(pReturnAddress - _countof(call), &call, _countof(call));

                    VirtualProtect(NtDllPatch.pPatchAddress, NtDllPatch.nPatchSize, dwProtect, &dwProtect);

                    NtDllPatch.bState = TRUE;
                }
            }
        }
    }
    return NtDllPatch.bState;
}

用於修補的 LdrpCallInitRoutine() 函數如下,詳見 vld.cpp 第 89~99 行

typedef BOOLEAN(NTAPI *PDLL_INIT_ROUTINE)(IN PVOID DllHandle, IN ULONG Reason, IN PCONTEXT Context OPTIONAL);
BOOLEAN WINAPI LdrpCallInitRoutine(IN PVOID BaseAddress, IN ULONG Reason, IN PVOID Context, IN PDLL_INIT_ROUTINE EntryPoint)
{
    LoaderLock ll;

    if (Reason == DLL_PROCESS_ATTACH) {
        g_vld.RefreshModules();
    }

    return EntryPoint(BaseAddress, Reason, (PCONTEXT)Context);
}

對預設的 LdrpCallInitRoutine() 函數修補完成後,在程式的後續運行過程中,每次新載入了 DLL 庫,都會自動執行 g_vld.RefreshModules(),刷新記憶體泄漏檢測的模塊列表。外部 API 介面 VLDRefreshModules() 也是對 g_vld.RefreshModules() 的一個簡單封裝(詳見 vldapi.cpp 第 95~98 行)。這個 g_vld.RefreshModules() 的流程可以簡述如下:

(1)使用 dbghelp.hEnumerateLoadedModulesW64 函數獲得當前進程的所有已載入模塊(DLLEXE),v2.5.1 使用的 dbghelp.dll 版本為 6.11.1.404

(2)遍歷已載入模塊,確保這些模塊的符號信息可用,使用到的 dbghelp.h 庫函數有:SymGetModuleInfoW64SymUnloadModule64SymLoadModuleExW。同時使用 IAT hook 技術替換掉這些模塊中的記憶體操作函數,達到監控所有記憶體操作的效果。

(3)保存當前所有已載入模塊的狀態及信息到 g_vldm_loadedModules 變數中,這是一個類似於 STL set 的數據結構,底層實現是紅黑樹。

3.2 通過 IAT hook 替換記憶體操作函數

這是載入 vld.dll 時做的第二件事,在 VisualLeakDetector 類構造函數中進行,詳見 vld.cpp 第 337~518 行,該構造函數的主幹如下。

// Constructor - Initializes private data, loads configuration options, and
//   attaches Visual Leak Detector to all other modules loaded into the current
//   process.
//
VisualLeakDetector::VisualLeakDetector ()
{
    _set_error_mode(_OUT_TO_STDERR);

    // Initialize configuration options and related private data.
    _wcsnset_s(m_forcedModuleList, MAXMODULELISTLENGTH, '\0', _TRUNCATE);
    m_maxDataDump    = 0xffffffff;
    m_maxTraceFrames = 0xffffffff;
    m_options        = 0x0;
    ...

    // Load configuration options.
    configure();
    if (m_options & VLD_OPT_VLDOFF) {
        Report(L"Visual Leak Detector is turned off.\n");
        return;
    }
    ...

    // Initialize global variables.
    g_currentProcess    = GetCurrentProcess();
    g_currentThread     = GetCurrentThread();
    g_processHeap       = GetProcessHeap();
    ...

    // Initialize remaining private data.
    m_heapMap         = new HeapMap;
    m_heapMap->reserve(HEAP_MAP_RESERVE);
    m_iMalloc         = NULL;
    ...

    // Initialize the symbol handler. We use it for obtaining source file/line
    // number information and function names for the memory leak report.
    LPWSTR symbolpath = buildSymbolSearchPath();
    ...
    if (!g_DbgHelp.SymInitializeW(g_currentProcess, symbolpath, FALSE)) {
        Report(L"WARNING: Visual Leak Detector: The symbol handler failed to initialize (error=%lu).\n"
            L"    File and function names will probably not be available in call stacks.\n", GetLastError());
    }
    delete [] symbolpath;
    ...

    // Attach Visual Leak Detector to every module loaded in the process.
    ...
    g_LoadedModules.EnumerateLoadedModulesW64(g_currentProcess, addLoadedModule, newmodules);
    attachToLoadedModules(newmodules);
    ModuleSet* oldmodules = m_loadedModules;
    m_loadedModules = newmodules;
    delete oldmodules;
    ...

    Report(L"Visual Leak Detector Version " VLDVERSION L" installed.\n");
    if (m_status & VLD_STATUS_FORCE_REPORT_TO_FILE) {
        // The report is being forced to a file. Let the human know why.
        Report(L"NOTE: Visual Leak Detector: Unicode-encoded reporting has been enabled, but the\n"
            L"  debugger is the only selected report destination. The debugger cannot display\n"
            L"  Unicode characters, so the report will also be sent to a file. If no file has\n"
            L"  been specified, the default file name is \"" VLD_DEFAULT_REPORT_FILE_NAME L"\".\n");
    }
    reportConfig();
}

重點在上面的第 47~53 行(對應 vld.cpp 第 494~502 行),這幾行的流程與 g_vld.RefreshModules() 的流程一樣,其中 attachToLoadedModules 的函數主幹如下,詳見 vld.cpp 第 769~906 行

VOID VisualLeakDetector::attachToLoadedModules (ModuleSet *newmodules)
{
    ...

    // Iterate through the supplied set, until all modules have been attached.
    for (ModuleSet::Iterator newit = newmodules->begin(); newit != newmodules->end(); ++newit)
    {
        ...
        DWORD64 modulebase = (DWORD64) (*newit).addrLow;
        ...
            
        // increase reference count to module
        HMODULE modulelocal = NULL;
        if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCTSTR) modulebase, &modulelocal))
            continue;
        ...

        // Attach to the module.
        PatchModule(modulelocal, m_patchTable, _countof(m_patchTable));
        ...
    }
}

m_patchTable 裡面存儲了需要進行 IAT hook 的記憶體操作函數表,詳見 dllspatches.cpp,下麵是一個概覽。

struct moduleentry_t
{
    LPCSTR          exportModuleName; // The name of the module exporting the patched API.
    BOOL            reportLeaks;      // Patch module to report leaks from it
    UINT_PTR        moduleBase;       // The base address of the exporting module (filled in at runtime when the modules are loaded).
    patchentry_t*   patchTable;
};

moduleentry_t VisualLeakDetector::m_patchTable [] = {
    // Win32 heap APIs.
    "kernel32.dll", FALSE,  0x0, m_kernelbasePatch, // we patch this record on Win7 and higher
    "kernel32.dll", FALSE,  0x0, m_kernel32Patch,

    // MFC new operators (exported by ordinal).
    "mfc42.dll",    TRUE,   0x0, mfc42Patch,
    "mfc42d.dll",   TRUE,   0x0, mfc42dPatch,
    "mfc42u.dll",   TRUE,   0x0, mfc42uPatch,
    "mfc42ud.dll",  TRUE,   0x0, mfc42udPatch,
    ...
    "mfc140.dll",   TRUE,   0x0, mfc140Patch,
    "mfc140d.dll",  TRUE,   0x0, mfc140dPatch,
    "mfc140u.dll",  TRUE,   0x0, mfc140uPatch,
    "mfc140ud.dll", TRUE,   0x0, mfc140udPatch,

    // CRT new operators and heap APIs.
    "msvcrt.dll",   FALSE,  0x0, msvcrtPatch,
    "msvcrtd.dll",  FALSE,  0x0, msvcrtdPatch,
    "msvcr70.dll",  FALSE,  0x0, msvcr70Patch,
    "msvcr70d.dll", FALSE,  0x0, msvcr70dPatch,
    ...
    "msvcr120.dll", FALSE,  0x0, msvcr120Patch,
    "msvcr120d.dll",FALSE,  0x0, msvcr120dPatch,
    "ucrtbase.dll", FALSE,  0x0, ucrtbasePatch,
    "ucrtbased.dll",FALSE,  0x0, ucrtbasedPatch,

    // NT APIs.
    "ntdll.dll",    FALSE,  0x0, m_ntdllPatch,

    // COM heap APIs.
    "ole32.dll",    FALSE,  0x0, m_ole32Patch
};

// This structure allows us to build a table of APIs which should be patched
// through to replacement functions provided by VLD.
struct patchentry_t
{
    LPCSTR  importName;       // The name (or ordinal) of the imported API being patched.
    LPVOID* original;         // Pointer to the original function.
    LPCVOID replacement;      // Pointer to the function to which the imported API should be patched through to.
};

static patchentry_t ucrtbasedPatch[] = {
    "_calloc_dbg",        &UCRTd::data.pcrtd__calloc_dbg,      UCRTd::crtd__calloc_dbg,
    "_malloc_dbg",        &UCRTd::data.pcrtd__malloc_dbg,      UCRTd::crtd__malloc_dbg,
    "_realloc_dbg",       &UCRTd::data.pcrtd__realloc_dbg,     UCRTd::crtd__realloc_dbg,
    ...
};

繼續跟蹤,其中 PatchModule 的函數主幹如下,詳見 utility.cpp 第 628~672 行,這個函數對 m_patchTable 中的每個表,都執行 PatchImport

BOOL PatchModule (HMODULE importmodule, moduleentry_t patchtable [], UINT tablesize)
{
    moduleentry_t *entry;
    UINT          index;
    BOOL          patched = FALSE;
    ...

    // Loop through the import patch table, individually patching each import
    // listed in the table.
    ...
    for (index = 0; index < tablesize; index++) {
        entry = &patchtable[index];
        if (PatchImport(importmodule, entry)) {
            patched = TRUE;
        }
    }

    return patched;
}

繼續跟蹤,到了 PatchImport 函數,詳見 utility.cpp 第 459~626 行,這是 IAT hook 技術的核心函數,正是在這個函數里,通過修改 IAT(導入地址表)將原先的記憶體操作函數替換為了 VLD 自定義的函數,修改 IAT 的核心代碼如下,其中 thunk->u1.FunctionIAT 表中原函數的地址,replacementVLD 自定義函數的地址,VirtualProtect 用於更改對應記憶體區域的讀寫屬性。

DWORD protect;
if (VirtualProtect(&thunk->u1.Function, sizeof(thunk->u1.Function), PAGE_EXECUTE_READWRITE, &protect)) {
    thunk->u1.Function = (DWORD_PTR)replacement;
    if (VirtualProtect(&thunk->u1.Function, sizeof(thunk->u1.Function), protect, &protect)) {
        ...
    }
}

除了對當前已載入的模塊進行 IAT hook 外,VisualLeakDetector 類構造函數還做了以下工作:

  • 初始化一系列全局的 NT APIs 函數句柄、全局變數、私有變數。
  • 初始化 VLD 的配置信息,調用 configure() 函數與 reportConfig() 函數。
  • 初始化符號搜索路徑,調用 buildSymbolSearchPath() 函數。

3.3 每次記憶體分配時獲取調用堆棧信息

CRT 中的 new 函數為例,VLD 會將其替換為以下自定義函數,詳見 crtmfcpatch.h 第 887~906 行

// crtd_scalar_new - Calls to the CRT's scalar new operator from msvcrXXd.dll
//   are patched through to this function.
//
//  - size (IN): The size, in bytes, of the memory block to be allocated.
//
//  Return Value:
//
//    Returns the value returned by the CRT scalar new operator.
//
template<int CRTVersion, bool debug>
void* CrtPatch<CRTVersion, debug>::crtd_scalar_new (size_t size)
{
    PRINT_HOOKED_FUNCTION();
    new_t pcrtxxd_scalar_new = (new_t)data.pcrtd_scalar_new;
    assert(pcrtxxd_scalar_new);

    CAPTURE_CONTEXT();
    CaptureContext cc((void*)pcrtxxd_scalar_new, context_, debug, (CRTVersion >= 140));
    return pcrtxxd_scalar_new(size);
}

CAPTURE_CONTEXT() 巨集定義如下,用於捕獲此次分配的指令地址,為後面獲取調用堆棧做準備,詳見 utility.h 第 74~97 行

// Capture current context
#if defined(_M_IX86)
#define CAPTURE_CONTEXT()                                                       \
    context_t context_;                                                         \
    {CONTEXT _ctx;                                                              \
    RtlCaptureContext(&_ctx);                                                   \
    context_.Ebp = _ctx.Ebp; context_.Esp = _ctx.Esp; context_.Eip = _ctx.Eip;  \
    context_.fp = (UINT_PTR)_ReturnAddress();}
#define GET_RETURN_ADDRESS(context)  (context.fp)
#elif defined(_M_X64)
#define CAPTURE_CONTEXT()                                                       \
    context_t context_;                                                         \
    {CONTEXT _ctx;                                                              \
    RtlCaptureContext(&_ctx);                                                   \
    context_.Rbp = _ctx.Rbp; context_.Rsp = _ctx.Rsp; context_.Rip = _ctx.Rip;  \
    context_.fp = (UINT_PTR)_ReturnAddress();}
#define GET_RETURN_ADDRESS(context)  (context.fp)
#else
// If you want to retarget Visual Leak Detector to another processor
// architecture then you'll need to provide an architecture-specific macro to
// obtain the frame pointer (or other address) which can be used to obtain the
// return address and stack pointer of the calling frame.
#error "Visual Leak Detector is not supported on this architecture."
#endif // _M_IX86 || _M_X64

CaptureContext 的構造函數與析構函數如下,詳見 vld.cpp 第 2903~2956 行

CaptureContext::CaptureContext(void* func, context_t& context, BOOL debug, BOOL ucrt) : m_context(context) {
    context.func = reinterpret_cast<UINT_PTR>(func);
    m_tls = g_vld.getTls();

    if (debug) {
        m_tls->flags |= VLD_TLS_DEBUGCRTALLOC;
    }

    if (ucrt) {
        m_tls->flags |= VLD_TLS_UCRT;
    }

    m_bFirst = (GET_RETURN_ADDRESS(m_tls->context) == NULL);
    if (m_bFirst) {
        // This is the first call to enter VLD for the current allocation.
        // Record the current frame pointer.
        m_tls->context = m_context;
    }
}

CaptureContext::~CaptureContext() {
    if (!m_bFirst)
        return;

    if ((m_tls->blockWithoutGuard) && (!IsExcludedModule())) {
        blockinfo_t* pblockInfo = NULL;
        if (m_tls->newBlockWithoutGuard == NULL) {
            g_vld.mapBlock(m_tls->heap,
                m_tls->blockWithoutGuard,
                m_tls->size,
                (m_tls->flags & VLD_TLS_DEBUGCRTALLOC) != 0,
                (m_tls->flags & VLD_TLS_UCRT) != 0,
                m_tls->threadId,
                pblockInfo);
        }
        else {
            g_vld.remapBlock(m_tls->heap,
                m_tls->blockWithoutGuard,
                m_tls->newBlockWithoutGuard,
                m_tls->size,
                (m_tls->flags & VLD_TLS_DEBUGCRTALLOC) != 0,
                (m_tls->flags & VLD_TLS_UCRT) != 0,
                m_tls->threadId,
                pblockInfo, m_tls->context);
        }

        CallStack* callstack = CallStack::Create();
        callstack->getStackTrace(g_vld.m_maxTraceFrames, m_tls->context);
        pblockInfo->callStack.reset(callstack);
    }

    // Reset thread local flags and variables for the next allocation.
    Reset();
}

CaptureContext 析構函數里,通過調用 g_vld.mapBlock()g_vld.remapBlock() 將此次分配的信息存入 m_heapMap,這是一個類似於 STL map 的數據結構,底層實現是紅黑樹,詳見 vldint.h,這裡面存儲了此次分配的線程 ID、分配序號、分配大小、所在堆等信息。

// Data is collected for every block allocated from any heap in the process.
// The data is stored in this structure and these structures are stored in
// a BlockMap which maps each of these structures to its corresponding memory
// block.
struct blockinfo_t {
    std::unique_ptr<CallStack> callStack;
    DWORD      threadId;
    SIZE_T     serialNumber;
    SIZE_T     size;
    bool       reported;
    bool       debugCrtAlloc;
    bool       ucrt;
};

// BlockMaps map memory blocks (via their addresses) to blockinfo_t structures.
typedef Map<LPCVOID, blockinfo_t*> BlockMap;

// Information about each heap in the process is kept in this map. Primarily
// this is used for mapping heaps to all of the blocks allocated from those
// heaps.
struct heapinfo_t {
    BlockMap blockMap;   // Map of all blocks allocated from this heap.
    UINT32   flags;      // Heap status flags
};

// HeapMaps map heaps (via their handles) to BlockMaps.
typedef Map<HANDLE, heapinfo_t*> HeapMap;

class VisualLeakDetector : public IMalloc
{
    ...
private:
    ...
    HeapMap             *m_heapMap; // Map of all active heaps in the process.
    ...
};

此外,CaptureContext 析構函數中還調用 getStackTrace() 獲取調用堆棧信息(一系列指令地址),根據用戶的不同配置,獲取堆棧有兩種方法,分別是 fast 模式與 safe 模式(詳見 配置項 StackWalkMethod)。閱讀源碼可知,詳見 callstack.cpp 第 605~771 行fast 模式使用 RtlCaptureStackBackTrace 函數來回溯堆棧,快但可能會漏;safe 模式使用 StackWalk64 函數來跟蹤堆棧,慢卻詳細。

VOID FastCallStack::getStackTrace (UINT32 maxdepth, const context_t& context)
{
    ...
    maxframes = RtlCaptureStackBackTrace(0, maxframes, reinterpret_cast<PVOID*>(myFrames), &BackTraceHash);
    ...
}

VOID SafeCallStack::getStackTrace (UINT32 maxdepth, const context_t& context)
{
    ...
    // Walk the stack.
    while (count < maxdepth) {
        count++;
        ...
        if (!g_DbgHelp.StackWalk64(architecture, g_currentProcess, g_currentThread, &frame, &currentContext, NULL,
            SymFunctionTableAccess64, SymGetModuleBase64, NULL, locker)) {
                // 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 CallStack.
        push_back((UINT_PTR)frame.AddrPC.Offset);
    }
}

3.4 生成泄漏檢測報告

v1.0 舊版本不同的是,新版本可以在運行過程中調用外部介面 VLDReportLeaks()VLDReportThreadLeaks() 即刻輸出泄漏報告,不必等到程式退出時。它們分別是 g_vld.ReportLeaks()g_vld.ReportThreadLeaks() 的簡單封裝,詳見 vldapi.cpp 第 65~73 行。對應的函數代碼如下,詳見 vld.cpp 第 2394~2434 行

SIZE_T VisualLeakDetector::ReportLeaks( )
{
    if (m_options & VLD_OPT_VLDOFF) {
        // VLD has been turned off.
        return 0;
    }

    // Generate a memory leak report for each heap in the process.
    SIZE_T leaksCount = 0;
    CriticalSectionLocker<> cs(g_heapMapLock);
    bool firstLeak = true;
    Set<blockinfo_t*> aggregatedLeaks;
    for (HeapMap::Iterator heapit = m_heapMap->begin(); heapit != m_heapMap->end(); ++heapit) {
        HANDLE heap = (*heapit).first;
        UNREFERENCED_PARAMETER(heap);
        heapinfo_t* heapinfo = (*heapit).second;
        leaksCount += reportLeaks(heapinfo, firstLeak, aggregatedLeaks);
    }
    return leaksCount;
}

SIZE_T VisualLeakDetector::ReportThreadLeaks( DWORD threadId )
{
    if (m_options & VLD_OPT_VLDOFF) {
        // VLD has been turned off.
        return 0;
    }

    // Generate a memory leak report for each heap in the process.
    SIZE_T leaksCount = 0;
    CriticalSectionLocker<> cs(g_heapMapLock);
    bool firstLeak = true;
    Set<blockinfo_t*> aggregatedLeaks;
    for (HeapMap::Iterator heapit = m_heapMap->begin(); heapit != m_heapMap->end(); ++heapit) {
        HANDLE heap = (*heapit).first;
        UNREFERENCED_PARAMETER(heap);
        heapinfo_t* heapinfo = (*heapit).second;
        leaksCount += reportLeaks(heapinfo, firstLeak, aggregatedLeaks, threadId);
    }
    return leaksCount;
}

通過上面這段源碼可知,輸出泄漏報告時,是遍歷 m_heapMap 逐堆(heap)進行輸出的,兩者的差別僅在於調用 reportLeaks() 函數時第四個參數值不同,ReportLeaks() 傳的是預設值 threadId = (DWORD)-1 ,而 ReportThreadLeaks() 傳的是目標線程的 threadId。繼續跟蹤,到了 reportLeaks() 函數,核心代碼如下,詳見 vld.cpp 第 1824~1932 行

SIZE_T VisualLeakDetector::reportLeaks (heapinfo_t* heapinfo, bool &firstLeak, Set<blockinfo_t*> &aggregatedLeaks, DWORD threadId)
{
    BlockMap* blockmap   = &heapinfo->blockMap;
    SIZE_T leaksFound = 0;

    for (BlockMap::Iterator blockit = blockmap->begin(); blockit != blockmap->end(); ++blockit)
    {
        // Found a block which is still in the BlockMap. We've identified a
        // potential memory leak.
        LPCVOID block = (*blockit).first;
        blockinfo_t* info = (*blockit).second;
        if (info->reported)
            continue;

        if (threadId != ((DWORD)-1) && info->threadId != threadId)
            continue;

        ...

        // It looks like a real memory leak.
        if (firstLeak) { // A confusing way to only display this message once
            Report(L"WARNING: Visual Leak Detector detected memory leaks!\n");
            firstLeak = false;
        }
        SIZE_T blockLeaksCount = 1;
        Report(L"---------- Block %Iu at " ADDRESSFORMAT L": %Iu bytes ----------\n", info->serialNumber, address, size);
		
        ...

        DWORD callstackCRC = 0;
        if (info->callStack)
            callstackCRC = CalculateCRC32(info->size, info->callStack->getHashValue());
        Report(L"  Leak Hash: 0x%08X, Count: %Iu, Total %Iu bytes\n", callstackCRC, blockLeaksCount, size * blockLeaksCount);
        leaksFound += blockLeaksCount;

        // Dump the call stack.
        if (blockLeaksCount == 1)
            Report(L"  Call Stack (TID %u):\n", info->threadId);
        else
            Report(L"  Call Stack:\n");
        if (info->callStack)
            info->callStack->dump(m_options & VLD_OPT_TRACE_INTERNAL_FRAMES);

        // Dump the data in the user data section of the memory block.
        if (m_maxDataDump != 0) {
            Report(L"  Data:\n");
            if (m_options & VLD_OPT_UNICODE_REPORT) {
                DumpMemoryW(address, (m_maxDataDump < size) ? m_maxDataDump : size);
            }
            else {
                DumpMemoryA(address, (m_maxDataDump < size) ? m_maxDataDump : size);
            }
        }
        Report(L"\n\n");
    }

    return leaksFound;
}

reportLeaks() 函數里,又對每個堆的 BlockMap 進行了遍歷(它也是一個類似於 STL map 的數據結構),這裡面存儲了在該堆上分配的所有記憶體塊信息,記憶體塊地址為 first key,相應的分配信息結構體為 second value

(1)Leak Hash 的計算:由以下調用方式及函數定義(詳見 utility.cpp 第 1085~1145 行)可知,這個值由泄露塊大小及其調用堆棧決定。進一步跟蹤表明,這個值還可能與堆棧獲取方式(fast 還是 safe)有關,因為不同方式下得到的 startValue 不同(進行 CRC 計算的初值不同)。

DWORD CalculateCRC32(UINT_PTR p, UINT startValue)
{
    register DWORD hash = startValue;
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >>  0) & 0xff)];
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >>  8) & 0xff)];
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >> 16) & 0xff)];
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >> 24) & 0xff)];
#ifdef WIN64
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >> 32) & 0xff)];
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >> 40) & 0xff)];
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >> 48) & 0xff)];
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ ((p >> 56) & 0xff)];
#endif
    return hash;
}

callstackCRC = CalculateCRC32(info->size, info->callStack->getHashValue());

(2)Call Stack 的符號化:通過下麵這一行調用 dump() 函數。

info->callStack->dump(m_options & VLD_OPT_TRACE_INTERNAL_FRAMES);

dump() 函數中,又調用 resolve() 函數對調用堆棧進行解析,將一系列指令地址轉換為文件名、函數名、行號等信息,詳見 callstack.cpp 第 345~468 行,其核心代碼如下。

int CallStack::resolve(BOOL showInternalFrames)
{
    ...

    // Iterate through each frame in the call stack.
    for (UINT32 frame = 0; frame < m_size; frame++)
    {
        // Try to get the source file and line number associated with
        // this program counter address.
        SIZE_T programCounter = (*this)[frame];
        if (GetCallingModule(programCounter) == g_vld.m_vldBase)
            continue;

        DWORD64 displacement64;
        BYTE symbolBuffer[sizeof(SYMBOL_INFO) + MAX_SYMBOL_NAME_SIZE];
        LPCWSTR functionName = getFunctionName(programCounter, displacement64, (SYMBOL_INFO*)&symbolBuffer, locker);

        ...

        BOOL foundline = g_DbgHelp.SymGetLineFromAddrW64(g_currentProcess, programCounter, &displacement, &sourceInfo, locker);

        ...

        if (!foundline)
            displacement = (DWORD)displacement64;
        NumChars = resolveFunction( programCounter, foundline ? &sourceInfo : NULL,
            displacement, functionName, stack_line, _countof( stack_line ));

        ...
    } // end for loop

    m_status |= CALLSTACK_STATUS_NOTSTARTUPCRT;
    return unresolvedFunctionsCount;
}

使用 SymGetLineFromAddrW64 介面獲得源文件名和行號,在 getFunctionName() 函數中調用 SymFromAddrW 介面獲得函數名,這兩點與 v1.0 的做法一致。在 resolveFunction() 中,使用 GetModuleFileName 介面獲得模塊名,並對堆棧信息字元串進行了格式化。

(3)Data 的格式化顯示:通過對 DumpMemoryW()DumpMemoryA() 的調用來將記憶體中的數據轉換為十六進位、ASCII 碼或 Unicode 碼,詳見 utility.cpp 第 48~190 行DumpMemoryA() 中與編碼轉換相關的核心代碼如下,通過強制類型轉換完成 ((PBYTE)address)[byteIndex],然後根據 isgraph() 函數的返回值來判斷是否能顯示該字元。

VOID DumpMemoryA (LPCVOID address, SIZE_T size)
{
    // Each line of output is 16 bytes.
    SIZE_T dumpLen;
    if ((size % 16) == 0) {
        // No padding needed.
        dumpLen = size;
    }
    else {
        // We'll need to pad the last line out to 16 bytes.
        dumpLen = size + (16 - (size % 16));
    }

    ...
    WCHAR  ascDump [18] = {0};
    ...
    for (SIZE_T byteIndex = 0; byteIndex < dumpLen; byteIndex++) {
        SIZE_T wordIndex = byteIndex % 16;
        ...
        SIZE_T ascIndex = wordIndex + wordIndex / 8;  
        if (byteIndex < size) {
            BYTE byte = ((PBYTE)address)[byteIndex];
            ...
            if (isgraph(byte)) {
                ascDump[ascIndex] = (WCHAR)byte;
            }
            else {
                ascDump[ascIndex] = L'.';
            }
        }
        ...
    }
}

DumpMemoryW() 中與編碼轉換相關的核心代碼如下,WORDunsigned short 的別名,先通過強制類型轉換將記憶體中的相鄰兩位元組轉為一個 WORD,然後直接將其賦值給 WCHAR 數組中的單個元素。

VOID DumpMemoryW (LPCVOID address, SIZE_T size)
{
    // Each line of output is 16 bytes.
    SIZE_T dumpLen;
    if ((size % 16) == 0) {
        // No padding needed.
        dumpLen = size;
    }
    else {
        // We'll need to pad the last line out to 16 bytes.
        dumpLen = size + (16 - (size % 16));
    }

    ...
    WCHAR  unidump [18] = {0};
    ...
    for (SIZE_T byteIndex = 0; byteIndex < dumpLen; byteIndex++) {
        ...
        SIZE_T uniIndex = ((byteIndex / 2) % 8) + ((byteIndex / 2) % 8) / 8; 
        if (byteIndex < size) {
            ...
            if (((byteIndex % 2) == 0) && ((byteIndex + 1) < dumpLen)) {
                // On every even byte, print one character.
                WORD   word = ((PWORD)address)[byteIndex / 2];
                if ((word == 0x0000) || (word == 0x0020)) {
                    unidump[uniIndex] = L'.';
                }
                else {
                    unidump[uniIndex] = word;
                }
            }
        }
        ...
    }
}

(4)輸出泄漏檢測報告Report() 函數里(詳見 utility.cpp 第 747~774 行),完成字元串的格式化後,又接著調用 Print() 輸出泄漏報告(詳見 utility.cpp 第 687~745 行),在這裡面會嘗試調用用戶自定義的 ReportHook() 函數,若沒有,則 CallReportHook() 預設返回 0

VOID Print (LPWSTR messagew)
{
    if (NULL == messagew)
        return;

    int hook_retval=0;
    if (!CallReportHook(0, messagew, &hook_retval))
    {
        if (s_reportEncoding == unicode) {
            if (s_reportFile != NULL) {
                // Send the report to the previously specified file.
                fwrite(messagew, sizeof(WCHAR), wcslen(messagew), s_reportFile);
            }

            if ( s_reportToStdOut )
                fputws(messagew, stdout);
        }
        else {
            const size_t MAXMESSAGELENGTH = 5119;
            size_t  count = 0;
            CHAR    messagea [MAXMESSAGELENGTH + 1];
            if (wcstombs_s(&count, messagea, MAXMESSAGELENGTH + 1, messagew, _TRUNCATE) != 0) {
                // Failed to convert the Unicode message to ASCII.
                assert(FALSE);
                return;
            }
            messagea[MAXMESSAGELENGTH] = '\0';

            if (s_reportFile != NULL) {
                // Send the report to the previously specified file.
                fwrite(messagea, sizeof(CHAR), strlen(messagea), s_reportFile);
            }

            if ( s_reportToStdOut )
                fputs(messagea, stdout);
        }

        if (s_reportToDebugger)
            OutputDebugStringW(messagew);
    }
    else if (hook_retval == 1)
        __debugbreak();

    if (s_reportToDebugger && (s_reportDelay)) {
        Sleep(10); // Workaround the Visual Studio 6 bug where debug strings are sometimes lost if they're sent too fast.
    }
}

3.5 程式退出時的工作

卸載 vld.dll 時,做了兩件事:先執行 VisualLeakDetector 類析構函數(在 _CRT_INIT() 中)、然後執行 NtDllRestore() 函數。首先看 VisualLeakDetector 類析構函數,詳見 vld.cpp 第 610~722 行,其函數主幹如下。

VisualLeakDetector::~VisualLeakDetector ()
{
    ...

    if (m_status & VLD_STATUS_INSTALLED) {
        // Detach Visual Leak Detector from all previously attached modules.
        ...
        g_LoadedModules.EnumerateLoadedModulesW64(g_currentProcess, detachFromModule, NULL);
        ...

        BOOL threadsactive = waitForAllVLDThreads();

        if (m_status & VLD_STATUS_NEVER_ENABLED) {
            // Visual Leak Detector started with leak detection disabled and
            // it was never enabled at runtime. A lot of good that does.
            Report(L"WARNING: Visual Leak Detector: Memory leak detection was never enabled.\n");
        }
        else {
            // Generate a memory leak report for each heap in the process.
            SIZE_T leaks_count = ReportLeaks();

            // Show a summary.
            if (leaks_count == 0) {
                Report(L"No memory leaks detected.\n");
            }
            else {
                Report(L"Visual Leak Detector detected %Iu memory leak", leaks_count);
                Report((leaks_count > 1) ? L"s (%Iu bytes).\n" : L" (%Iu bytes).\n", m_curAlloc);
                Report(L"Largest number used: %Iu bytes.\n", m_maxAlloc);
                Report(L"Total allocations: %Iu bytes.\n", m_totalAlloc);
            }
        }

        // Free resources used by the symbol handler.
        DbgTrace(L"dbghelp32.dll %i: SymCleanup\n", GetCurrentThreadId());
        if (!g_DbgHelp.SymCleanup(g_currentProcess)) {
            Report(L"WARNING: Visual Leak Detector: The symbol handler failed to deallocate resources (error=%lu).\n",
                GetLastError());
        }

        ...
        
        if (threadsactive) {
            Report(L"WARNING: Visual Leak Detector: Some threads appear to have not terminated normally.\n"
                L"  This could cause inaccurate leak detection results, including false positives.\n");
        }
        Report(L"Visual Leak Detector is now exiting.\n");

        ...

        checkInternalMemoryLeaks();
    }
    else {
        ...
    }
    ...
}

在析構函數中做了以下幾個工作:

(1)還原 IAT 表,將被替換的函數還原。調用堆棧為:EnumerateLoadedModulesW64 -> detachFromModule -> RestoreModule -> RestoreImport,詳見 RestoreImport 函數,在 utility.cpp 第 776~895 行,核心代碼為 iate->u1.Function = (DWORD_PTR)original

(2)等待其他線程退出。調用了 waitForAllVLDThreads() 函數,詳見 vld.cpp 第 520~565 行,如下所示,當有線程未退出時,程式可能會等待幾十秒(不大於 90 秒),這也是有些時候關閉程式但很久未輸出報告的原因。

bool VisualLeakDetector::waitForAllVLDThreads()
{
    bool threadsactive = false;
    DWORD dwCurProcessID = GetCurrentProcessId();
    int waitcount = 0;

    // See if any threads that have ever entered VLD's code are still active.
    CriticalSectionLocker<> cs(m_tlsLock);
    for (TlsMap::Iterator tlsit = m_tlsMap->begin(); tlsit != m_tlsMap->end(); ++tlsit) {
        if ((*tlsit).second->threadId == GetCurrentThreadId()) {
            // Don't wait for the current thread to exit.
            continue;
        }

        HANDLE thread = OpenThread(SYNCHRONIZE | THREAD_QUERY_INFORMATION, FALSE, (*tlsit).second->threadId);
        if (thread == NULL) {
            // Couldn't query this thread. We'll assume that it exited.
            continue; // XXX should we check GetLastError()?
        }
        if (GetProcessIdOfThread(thread) != dwCurProcessID) {
            //The thread ID has been recycled.
            CloseHandle(thread);
            continue;
        }
        if (WaitForSingleObject(thread, 10000) == WAIT_TIMEOUT) { // 10 seconds
            // There is still at least one other thread running. The CRT
            // will stomp it dead when it cleans up, which is not a
            // graceful way for a thread to go down. Warn about this,
            // and wait until the thread has exited so that we know it
            // can't still be off running somewhere in VLD's code.
            //
            // Since we've been waiting a while, let the human know we are
            // still here and alive.
            waitcount++;
            threadsactive = true;
            if (waitcount >= 9) // 90 sec.
            {
                CloseHandle(thread);
                return threadsactive;
            }
            Report(L"Visual Leak Detector: Waiting for threads to terminate...\n");
        }
        CloseHandle(thread);
    }
    return threadsactive;
}

(3)生成泄漏檢測報告。調用了 ReportLeaks() 函數,其實現思路詳見本博客上文。

(4)生成泄漏檢測總結信息leaks_count 為本次檢測出的全部泄漏塊總數,m_curAlloc 為本次檢測出的全部泄漏塊總大小,m_maxAlloc 為整個檢測過程中全部泄漏塊總大小的最大值(即 max(m_curAlloc)),m_totalAlloc 為整個檢測過程中在堆上所分配記憶體的總大小。

Report(L"Visual Leak Detector detected %Iu memory leak", leaks_count);
Report((leaks_count > 1) ? L"s (%Iu bytes).\n" : L" (%Iu bytes).\n", m_curAlloc);
Report(L"Largest number used: %Iu bytes.\n", m_maxAlloc);
Report(L"Total allocations: %Iu bytes.\n", m_totalAlloc);

(5)釋放資源。釋放內部成員變數的記憶體,使用 SymCleanup 釋放符號資源。

(6)泄漏自檢。調用了 checkInternalMemoryLeaks() 函數,詳見 vld.cpp 第 567~608 行。通過遍歷一個 VLD 自定義雙向鏈表來判斷自身是否產生了記憶體泄漏,這個雙向鏈表的結構與系統自帶的記憶體管理雙向鏈表相類似,可參考本人另一篇博客 核心源碼剖析(VLD 1.0)

析構完畢後,會執行 NtDllRestore() 函數,詳見 vld.cpp 第 261~279 行,還原對預設 LdrpCallInitRoutine() 的更改。

BOOL NtDllRestore(NTDLL_LDR_PATCH &NtDllPatch)
{
    // Restore patched bytes
    BOOL bResult = FALSE;
    if (NtDllPatch.bState && NtDllPatch.nPatchSize && &NtDllPatch.pBackup[0]) {
        DWORD dwProtect = 0;
        if (VirtualProtect(NtDllPatch.pPatchAddress, NtDllPatch.nPatchSize, PAGE_EXECUTE_READWRITE, &dwProtect)) {
            memcpy(NtDllPatch.pPatchAddress, NtDllPatch.pBackup, NtDllPatch.nPatchSize);
            VirtualProtect(NtDllPatch.pPatchAddress, NtDllPatch.nPatchSize, dwProtect, &dwProtect);

            if (VirtualProtect(NtDllPatch.pDetourAddress, NtDllPatch.nDetourSize, PAGE_EXECUTE_READWRITE, &dwProtect)) {
                memset(NtDllPatch.pDetourAddress, 0x00, NtDllPatch.nDetourSize);
                VirtualProtect(NtDllPatch.pDetourAddress, NtDllPatch.nDetourSize, dwProtect, &dwProtect);
                bResult = TRUE;
            }
        }
    }
    return bResult;
}

4. 其他問題

4.1 如何區分分配記憶體的來由

VLD 2.5.1 思路如下:

  • 核心源碼剖析(VLD 1.0) 一樣,使用 _CrtMemBlockHeader 結構體的 nBlockUse 成員來判斷是否屬於 CRT 分配的記憶體,詳見 resolveStacks() 函數(vld.cpp 第 2861~2862 行)、getLeaksCount() 函數(vld.cpp 第 1739~1740 行)、reportLeaks() 函數(vld.cpp 第 1854~1855 行)。

  • 通過調用堆棧中的函數名來判斷是否屬於 CRT 啟動代碼分配的記憶體,詳見 isCrtStartupFunction() 函數,在 callstack.cpp 第 513~554 行

  • VLD 仿照 _CrtMemBlockHeader 結構體自定義了一個 vldblockheader_t,用來存儲 VLD 內部的每次分配信息,詳見 vldheap.h 第 88~99 行。接著重載了內部的 new/delete 函數(詳見 vldheap.cpp)、自定義繼承了 std::allocator(詳見 vldallocator.h),併為 VLD 開闢了一個專屬堆 g_vldHeap。這樣一來,VLD 內部每次分配記憶體時都會分配在專屬堆 g_vldHeap 上,且都加上這個自定義頭,最終形成了一個存儲 VLD 內部記憶體分配信息的雙向鏈表,讓一個全局指針 g_vldBlockList 指向這個鏈表的頭節點,後續通過這個全局指針訪問雙向鏈表,即可獲得 VLD 內部的記憶體分配信息。

    // Memory block header structure used internally by VLD. All internally
    // allocated blocks are allocated from VLD's private heap and have this header
    // pretended to them.
    struct vldblockheader_t
    {
        struct vldblockheader_t *next;          // Pointer to the next block in the list of internally allocated blocks.
        struct vldblockheader_t *prev;          // Pointer to the preceding block in the list of internally allocated blocks.
        const char              *file;          // Name of the file where this block was allocated.
        int                      line;          // Line number within the above file where this block was allocated.
        size_t                   size;          // The size of this memory block, not including this header.
        size_t                   serialNumber;  // Each block is assigned a unique serial number, starting from zero.
    };
    

4.2 如何實現多線程檢測

核心源碼剖析(VLD 1.0) 一樣,v2.5.1 也使用到了線程本地存儲(Thread Local Storage),參考 MicroSoft-Using-Thread-Local-Storage。全局對象 g_vld 有兩個成員變數 m_tlsIndexm_tlsMap,相關定義可見 vldint.h,如下。

// Thread local storage structure. Every thread in the process gets its own copy
// of this structure. Thread specific information, such as the current leak
// detection status (enabled or disabled) and the address that initiated the
// current allocation is stored here.
struct tls_t {
    context_t	context;       	  // Address of return address at the first call that entered VLD's code for the current allocation.
    UINT32	    flags;            // Thread-local status flags:
#define VLD_TLS_DEBUGCRTALLOC 0x1 //   If set, the current allocation is a CRT allocation.
#define VLD_TLS_DISABLED 0x2 	  //   If set, memory leak detection is disabled for the current thread.
#define VLD_TLS_ENABLED  0x4 	  //   If set, memory leak detection is enabled for the current thread.
#define VLD_TLS_UCRT     0x8      //   If set, the current allocation is a UCRT allocation.
    UINT32	    oldFlags;         // Thread-local status old flags
    DWORD 	    threadId;         // Thread ID of the thread that owns this TLS structure.
    HANDLE      heap;
    LPVOID      blockWithoutGuard; // Store pointer to block.
    LPVOID      newBlockWithoutGuard;
    SIZE_T      size;
};

// The TlsSet allows VLD to keep track of all thread local storage structures
// allocated in the process.
typedef Map<DWORD,tls_t*> TlsMap;

class VisualLeakDetector : public IMalloc
{
    ...
private:
    ...
    DWORD  m_tlsIndex; // Thread-local storage index.
    ...
    TlsMap *m_tlsMap;  // Set of all thread-local storage structures for the process.
    ...
}

m_tlsIndex 用來接收 TlsAlloc() 返回的索引值,初始化成功後(詳見 vld.cpp 第 337~518 行),當前進程的任何線程都可以使用這個索引值來存儲和訪問對應線程本地的值,不同線程間互不影響,訪問獲得的結果也與其他線程無關,v2.5.1 用它來存儲一個 tls_t 結構體指針,這個結構體里與多線程檢測控制有關的變數有 flagsoldFlagsthreadId 這三個,其餘的被當做每次記憶體操作時的臨時變數。

m_tlsIndex        = TlsAlloc();
...
if (m_tlsIndex == TLS_OUT_OF_INDEXES) {
    Report(L"ERROR: Visual Leak Detector could not be installed because thread local"
        L"  storage could not be allocated.");
    return;
}

TlsMap 是一個類似於 STL map 的容器,線程 ID 為 first key,對應的 tls_t*second value,用它來管理每個線程的 tls_t 結構體記憶體。每次進行記憶體分配時,都會進入 enabled() 函數(詳見 vld.cpp 第 1210~1239 行)與 getTls() 函數(詳見 vld.cpp 第 1287~1325 行),這兩個函數都在分配行為所屬的線程中執行。

BOOL VisualLeakDetector::enabled ()
{
    if (!(m_status & VLD_STATUS_INSTALLED)) {
        // Memory leak detection is not yet enabled because VLD is still
        // initializing.
        return FALSE;
    }

    tls_t* tls = getTls();
    if (!(tls->flags & VLD_TLS_DISABLED) && !(tls->flags & VLD_TLS_ENABLED)) {
        // The enabled/disabled

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。。 本文作者:奇銘(掘金) 需求背景 前段時間,離線計算產品接到改造數據同步表單的需求。 一方面,數據同步模塊的代碼可讀性和可維護性比較差,相對應的,在數據同步模塊開發新 ...
  • <meta> 標簽是 HTML 中用於描述網頁元信息的元素。它位於 <head> 部分,不會顯示在頁面內容中,但對於瀏覽器、搜索引擎等具有重要作用。主要作用有:定義文檔的字元編碼、提供網頁的描述信息、關鍵詞、作者、視口設置等,這些信息有助於搜索引擎理解和索引網頁內容。 <meta> 標簽的主要屬性有 ...
  • 當前,前端對二進位數據有許多的API可以使用,這豐富了前端對文件數據的處理能力,有了這些能力,就能夠對圖片等文件的數據進行各種處理。 本文將著重介紹一些前端二進位數據處理相關的API知識,如Blob、File、FileReader、ArrayBuffer、TypeArray、DataView等等。 ...
  • 模式動機 原型模式(Prototype Pattern)結構較為簡單,它是一種特殊的創建型模式,當需要創建大量相同或者相似對象時,可以通過對一個已有對象的複製獲取更多對象。Java語言提供了較為簡單的原型模式解決方案,只需要創建一個原型對象,然後通過在類中定義的克隆方法複製自己。該模式應用較為廣泛, ...
  • 開發環境 以下是我的開發環境 JDK 1.8 Maven 3.6.3 IDEA 2019(2019 無所畏懼,即使現在已經 2023 年了哈哈哈) 使用 Maven 的方式創建 Spring Boot 項目 下麵的內容可能會因 IDEA 版本不同,而有些選項不同,但是大同小異。 1. 打開 IDEA ...
  • 大家是否見過這種for迴圈,在for迴圈前加了個標記的: outerLoop: for (; ; ) { for (; ; ) { break outerLoop; } } 我之前有一次在公司業務代碼中見過有這種寫法的,沒在意,今天在看JDK線程池的代碼時,又看到ThreadPoolExecutor ...
  • 大家好,3y啊。好些天沒更新了,並沒有偷懶,只不過一直在安裝環境,差點都想放棄了。 上一次比較大的更新是做了austin的預覽地址,把企業微信的應用和機器人消息各種的消息類型和功能給完善了。上一篇文章也提到了,austin常規的功能已經更新得差不多了,剩下的就是各種細節的完善。 不知道大家還記不記得 ...
  • 上一篇咱們介紹了 Hibernate 以及寫了一個 Hibernate 的工具類,快速入門體驗了一波 Hibernate 的使用,我們只需通過 Session 對象就能實現資料庫的操作了。 現在,這篇介紹使用 Hibernate 進行基本的 CRUD、懶載入以及緩存的知識。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...