让Visual Leak Detector使用最新10.0版本的dbghelp.dll
让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()还有限制,参看其文档。