发布时间:2022-12-04 文章分类:编程知识 投稿人:李佳 字号: 默认 | | 超大 打印

让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_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