主頁 > 後端開發 > 【Visual Leak Detector】核心原始碼剖析(VLD 2.5.1)

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

2023-05-11 07:32:18 後端開發

說明

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

目錄
  • 說明
  • 1. 原始碼獲取
  • 2. 原始碼檔案概覽
  • 3. 原始碼剖析
    • 3.1 通過 inline hook 修補 LdrpCallInitRoutine
    • 3.2 通過 IAT hook 替換記憶體操作函式
    • 3.3 每次記憶體分配時獲取呼叫堆疊資訊
    • 3.4 生成泄漏檢測報告
    • 3.5 程式退出時的作業
  • 4. 其他問題
    • 4.1 如何區分分配記憶體的來由
    • 4.2 如何實作多執行緒檢測
    • 4.3 如何實作雙擊輸出自動定位到指定行


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 的原始碼進行剖析,以下資料可能對理解其檢測原理有幫助:

  • 博客園-Visual Leak Detector 2.3 原理剖析,
  • 騰訊云-HOOK技術實戰,
  • 博客園-臨界區(Critical Section)的封裝和使用示例,
  • 博客園-讓Visual Leak Detector使用最新10.0版本的dbghelp.dll,
  • StackExchange-How to hook the entry point of a DLL,
  • MSDN-DLL 和 Visual C++ 運行時庫行為,
  • MSDN-運行庫行為,
  • MSDN-Use the C Run-Time,
  • 博客園-inline hook 原理&教程,

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.h 庫 EnumerateLoadedModulesW64 函式獲得當前行程的所有已加載模塊(DLLEXE),v2.5.1 使用的 dbghelp.dll 版本為 6.11.1.404

(2)遍歷已加載模塊,確保這些模塊的符號資訊可用,使用到的 dbghelp.h 庫函式有:SymGetModuleInfoW64、SymUnloadModule64、SymLoadModuleExW,同時使用 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 state for the current thread has not been
        // initialized yet. Use the default state.
        if (m_options & VLD_OPT_START_DISABLED) {
            tls->flags |= VLD_TLS_DISABLED;
        }
        else {
            tls->flags |= VLD_TLS_ENABLED;
        }
    }

    return ((tls->flags & VLD_TLS_ENABLED) != 0);
}

tls_t* VisualLeakDetector::getTls ()
{
    // Get the pointer to this thread's thread local storage structure.
    tls_t* tls = (tls_t*)TlsGetValue(m_tlsIndex);
    assert(GetLastError() == ERROR_SUCCESS);

    if (tls == NULL) {
        DWORD threadId = GetCurrentThreadId();

        CriticalSectionLocker<> cs(m_tlsLock);
        TlsMap::Iterator it = m_tlsMap->find(threadId);
        if (it == m_tlsMap->end()) {
            // This thread's thread local storage structure has not been allocated.
            tls = new tls_t;

            // Add this thread's TLS to the TlsSet.
            m_tlsMap->insert(threadId, tls);
        } else {
            // Already had a thread with this ID
            tls = (*it).second;
        }

        ZeroMemory(&tls->context, sizeof(tls->context));
        tls->flags = 0x0;
        tls->oldFlags = 0x0;
        tls->threadId = threadId;
        tls->blockWithoutGuard = NULL;
        TlsSetValue(m_tlsIndex, tls);
    }

    return tls;
}

若是第一次進入,會給當前執行緒分配一個 tls_t 結構體,并初始化結構體的成員變數,若用戶設定了 VLD_OPT_START_DISABLED,則當前執行緒初始值 tls->flags |= VLD_TLS_DISABLED,表示 VLD 對當前執行緒關閉,否則 tls->flags |= VLD_TLS_ENABLED,表示 VLD 對當前執行緒開啟,

4.3 如何實作雙擊輸出自動定位到指定行

這個實作起來比較簡單,只要保證這一行輸出中,前面的字串形式為 "檔案路徑(行號)" 就可以,vld 的堆疊輸出形式正好符合這個要求,因此可以自動跳轉,參考 CppBlog - 如何在 vs 中的 Output 視窗雙擊定位代碼,下面是這個功能的一個演示例:

#include <Windows.h>

int main()
{
    OutputDebugString(L" e:\\Cworkspace\\VSDemo\\testDoubleClick\\testDoubleClick\\main.cpp (3).\n");

    return 0;
}

本文作者:木三百川

本文鏈接:https://www.cnblogs.com/young520/p/17389224.html

著作權宣告:本文系博主原創文章,著作權歸作者所有,商業轉載請聯系作者獲得授權,非商業轉載請附上出處鏈接,遵循 署名-非商業性使用-相同方式共享 4.0 國際版 (CC BY-NC-SA 4.0) 著作權協議,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552101.html

標籤:其他

上一篇:34基于Java的學生選課系統或學生課程管理系統

下一篇:返回列表

標籤雲
其他(158771) Python(38125) JavaScript(25413) Java(18025) C(15225) 區塊鏈(8263) C#(7972) AI(7469) 爪哇(7425) MySQL(7175) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5338) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4570) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2432) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1972) 功能(1967) Web開發(1951) HtmlCss(1935) python-3.x(1918) 弹簧靴(1913) C++(1913) xml(1889) PostgreSQL(1875) .NETCore(1860) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 【Visual Leak Detector】核心原始碼剖析(VLD 2.5.1)

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

    uj5u.com 2023-05-11 07:32:18 more
  • 34基于Java的學生選課系統或學生課程管理系統

    基于java的學生課程管理系統,基于java的學生選課系統,javaWeb的學生選課系統,學生成績管理系統,課表管理系統,學院管理系統,大學生選課系統設計與實作,網上選課系統,課程成績打分。 ......

    uj5u.com 2023-05-11 07:32:13 more
  • 最佳實踐:路徑路由匹配規則的設計與實作

    本文設計并實作了一種專用于路徑路由匹配的規則,以一種簡單而通用的方式描述一組路徑的特征,來簡化這種場景路由描述難度,讓小白可以快速學習并上手。 ......

    uj5u.com 2023-05-11 07:25:44 more
  • 文盤Rust —— rust連接oss | 京東云技術團隊

    物件存盤是云的基礎組件之一,各大云廠商都有相關產品。這里跟大家介紹一下rust與物件存盤交到的基本套路和其中的一些技巧。 ......

    uj5u.com 2023-05-10 10:44:00 more
  • Java的抽象類 & 介面

    抽象類:在子類繼承父類時,父類的一些方法實作是不明確的(父類對子類的實作一無所知)。這時需要使父類是抽象類,在子類中提供方法的實作。

    介面(interface)技術主要用來描述類具有什么功能,而并不給出每個功能的具體實作。 ......

    uj5u.com 2023-05-10 10:38:48 more
  • 【11個適合畢設的Python可視化大屏】用pyecharts開發拖拽式可視

    你好,我是@馬哥python說,一枚10年程式猿。 一、效果演示 以下是我近期用Python開發的原創可視化資料分析大屏,非常適合畢設用,下面逐一展示:(以下是截圖,實際上有動態互動效果哦) 以下大屏均為@馬哥python說的個人原創,請勿轉載。 1.1 影視劇分析大屏 1.2 豆瓣電影分析大屏A ......

    uj5u.com 2023-05-10 10:27:16 more
  • 【11個適合畢設的Python可視化大屏】用pyecharts開發拖拽式可視

    你好,我是@馬哥python說,一枚10年程式猿。 一、效果演示 以下是我近期用Python開發的原創可視化資料分析大屏,非常適合畢設用,下面逐一展示:(以下是截圖,實際上有動態互動效果哦) 以下大屏均為@馬哥python說的個人原創,請勿轉載。 1.1 影視劇分析大屏 1.2 豆瓣電影分析大屏A ......

    uj5u.com 2023-05-10 10:25:24 more
  • Java的抽象類 & 介面

    抽象類:在子類繼承父類時,父類的一些方法實作是不明確的(父類對子類的實作一無所知)。這時需要使父類是抽象類,在子類中提供方法的實作。

    介面(interface)技術主要用來描述類具有什么功能,而并不給出每個功能的具體實作。 ......

    uj5u.com 2023-05-10 10:24:35 more
  • C語言快速入門教程1快速入門 2指令 3條件選擇

    快速入門 什么是C語言? C是一種編程語言,1972年由Dennis Ritchie在美國AT & T的貝爾實驗室開發。C語言變得很流行,因為它很簡單,很容易使用。今天經常聽到的一個觀點是--"C語言已經被C++、Python和Java等語言所取代,所以今天何必再去學習C語言"。我很不贊同這種觀點。 ......

    uj5u.com 2023-05-10 10:23:23 more
  • 高效c語言1快速入門

    本章將開發你的第一個C語言程式:傳統的 "Hello, world!"程式。然后討論一些編輯器和編譯器的選項,并闡述移植性問題。 Hello, world! #include <stdio.h> #include <stdlib.h> int main(void) { puts("Hello, wo ......

    uj5u.com 2023-05-10 10:22:59 more