說明: 1. 本文基於Spring-Framework 5.1.x版本講解 2. 建議讀者對Mybatis有基本的使用經驗 概述 這一篇我們講講org.springframework.beans.factory.FactoryBean介面,這個介面功能非常強大,可以集成不同的中間件或組件到Sprin ...
讓Visual Leak Detector使用最新10.0版本的dbghelp.dll
介紹
VLD(Visual Leak Detector)是一個檢測Windows C++程式記憶體泄漏的老牌神器,但好幾年沒維護了。
網址:https://github.com/KindDragon/vld/
需求
這個工具通過SxS manifest綁定了只能使用它工程目錄下自帶的dbghelp.dll來處理pdb符號,版本是6.11.1.404。
這個版本目前比較老了,所以在解析VS2019/VS2022生成的pdb文件時,有時候會崩掉或者無法解析出調用棧的符號,導致無法報出來完整的記憶體泄漏,影響基本功能。所以需要升級它所使用的dbghelp.dll。
VLD的實現機制
在首次進入vld_x64.dll的PE入口時,inline hook掉ntdll.dll的LdrpCallInitRoutine()函數,因為此時可以假定vld_x64.dll是被ntdll.dll的LdrpCallInitRoutine()函數調用的。
這樣後續ntdll.dll調用當前進程中的任何dll的入口函數時,都會先調用vld_x64.dll提供的一個LdrpCallInitRoutine() hook函數。
完成hook後,會執行vld_x64.dll中的各個全局對象的構造。vld_x64.dll提供了一個全局對象g_vld,這個對象的構造函數會調用dbghelp.dll的SymInitializeW()來初始化MS的符號庫函數。
__declspec(dllexport) VisualLeakDetector g_vld;
在LdrpCallInitRoutine() hook函數中,VLD會刷新當前進程所載入的模塊列表,調用dbghelp.dll的SymLoadModuleExW()載入新dll的pdb符號。
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); }
問題
這樣看起來並無問題。但是10.0版本的dbghelp.dll相比6.11版本有一個改動,導致VLD現有的pdb符號解析功能失敗。
那就是10.0版本的SymInitializeW()的內部代碼會去載入某些DLL,這會導致走到LdrpCallInitRoutine() hook函數中去刷新模塊列表,並最終調用SymLoadModuleExW()。
也就是說SymInitializeW()在成功返回之前會去調用SymLoadModuleExW(),這顯然不符合MS的debug help API的約定,所以此時的SymLoadModuleExW()都會返回失敗,導致彙報泄漏時無法解析符號。
解決辦法
1、設置一個全局的bool標誌變數,在調用SymInitializeW()之前置位,調用完SymInitializeW()之後清除。
dbghelp.h:
extern volatile bool init; BOOL SymInitializeW(_In_ HANDLE hProcess, _In_opt_ PCWSTR UserSearchPath, _In_ BOOL fInvadeProcess) { init = true; CriticalSectionLocker<CriticalSection> cs(m_lock); const auto r = ::SymInitializeW(hProcess, UserSearchPath, fInvadeProcess); init = false; return r; }
2、在LdrpCallInitRoutine() hook函數中判斷一下,如果標誌被置位,則本次就不要刷新模塊列表了,也就不會去調用SymLoadModuleExW()。
vld.cpp:
volatile bool init = false; BOOLEAN WINAPI LdrpCallInitRoutine(IN PVOID BaseAddress, IN ULONG Reason, IN PVOID Context, IN PDLL_INIT_ROUTINE EntryPoint) { LoaderLock ll; if (Reason == DLL_PROCESS_ATTACH) { if (!init) g_vld.RefreshModules(); } return EntryPoint(BaseAddress, Reason, (PCONTEXT)Context); }
3、相應地,要刪掉VLD工程屬性中添加的和SxS有關的設置,如
- vld.dll.dependency.x64.manifest
- vld.dll.dependency.x86.manifest
- dbghelp.dll (6.11版本的)
- Microsoft.DTfW.DHL.manifest (6.11版本的)
- $(SolutionDir)\lib\dbghelp\lib\$(PlatformName) (不要依賴這個目錄下的lib)
這樣編譯出來的vld_x64.dll預設會載入system32下的dbghelp.dll。
也可以複製Windows SDK、VS2019/VS2022、windbg目錄下的dbghelp.dll,但不要忘了也複製同目錄下的那一堆api-ms-win-crt-runtime-l1-1-0.dll之類的CRT dll。
附贈
幾個防止記憶體泄漏的tips:
1、靜態鏈接到openssl時,需要在DLL_THREAD_DETACH時調用OPENSSL_thread_stop()釋放PTD(即per-thread-data)。
2、靜態鏈接到log4cplus時,需要在DLL_THREAD_DETACH時調用log4cplus::threadCleanup()釋放per-thread-data。
3、libzip有兩個坑(其文檔寫得不甚清楚):
一、zip_close()會順帶將關聯的zip source的句柄也關閉,所以對應的zip source句柄不要再單獨關閉。
如果要保留zip source句柄另作他用,需要在zip_close()之前先用zip_source_keep()將zip source的句柄引用計數加1。
二、zip source句柄用zip_source_free()釋放,而不是用zip_source_close()。
zip_source_free()還有限制,參看其文檔。
參考:
https://github.com/KindDragon/vld/issues/86