缘起
最近又遇到了一个程序功能不正常的问题,深入调查后发现与全局变量初始化顺序有非常大的关系,只不过这次更加隐蔽。
之前总结了两篇与全局变量初始化顺序有关的文章,感兴趣的小伙伴儿可以参考《调试实战 | dll 加载失败之全局变量初始化篇》 和《调试实战 | 全局变量初始化顺序探究》。
在排查错误之前先简单介绍一下相关代码。
示例程序
示例程序一共包含4 个工程:LoadDlls, dll1, dll2, dll3。
主程序
LoadDlls.exe会加载dll1.dlldll1.dll隐式依赖了dll2.dll,所以dll1.dll加载的时候会自动加载dll2.dlldll2.dll中的全局变量s_culprit的构造函数会加载dll3.dlldll3.dll加载的时候会自动调用dll2.dll的导出函数RegisterInitCallback()和RegisterCallback()
下面是每个工程的关键代码
src/common/autorunner.h该文件是公共头文件,实现了自动注册逻辑
// autorunner.h #pragma once class AutoRunner { public: AutoRunner(void (*func)()) { func(); } }; #define STR_CAT(s1, s2) s1 ## s2 #define NAME_WITH_LINE(name, line) STR_CAT(name, line) #define BEGIN_AUTO_RUN static AutoRunner NAME_WITH_LINE(s_auto_runner_, __LINE__) ([](){ #define END_AUTO_RUN });LoadDlls该工程只有一个源文件,用来模拟加载各种插件。对应的源码如下:
// LoadDlls.cpp #include "windows.h" #include <iostream> #include <map> #include <vector> typedef void (*PFN_Init)(); std::map<std::string, HMODULE> LoadPlugins(const char* plugins[]) { std::map<std::string, HMODULE> result; for (int idx = 0; ; ++idx) { constchar* plugin = plugins[idx]; if (plugin == nullptr) { break; } HMODULE module = LoadLibraryA(plugin); if (module == nullptr) { DWORD lastError = GetLastError(); std::cout << "[+] load [" << plugin << "] failed with error " << lastError << std::endl; } else { result[plugin] = module; } } return result; } void InitPlugins(const std::map<std::string, HMODULE>& loaded_plugins) { for (auto& it : loaded_plugins) { auto init_entry = (PFN_Init)GetProcAddress(it.second, "Init"); if (init_entry != nullptr) { init_entry(); } } } int main() { std::cout << "[+] load plugin start." << std::endl; constchar* plugins[] = { "dll1.dll", /*"dll2.dll", "dll3.dll",*/nullptr }; auto loaded_module_map = LoadPlugins(plugins); std::cout << "[+] load plugin done, press any key to init plugins." << std::endl; system("pause"); InitPlugins(loaded_module_map); return0; }dll1该工程非常简单,什么有用的事情都没做,但是会依赖
dll2,加载dll1.dll的时候会自动加载dll2.dll。// dllmain1.cpp #include <windows.h> #include <iostream> #include "../common/autorunner.h" #include "../dll2/exports.h" void PrintMajorVersion() { std::cout << "Major Version of dll2.dll is " << MajorVersion() << std::endl; } BEGIN_AUTO_RUN std::cout << "I'm running in dll1.dll, which implicitly depends on dll2.dll." << std::endl; END_AUTO_RUNdll2该模块提供了注册回调函数的导出接口,并实现了回调逻辑
// exports.h #pragma once #ifdef DLL_EXPORT_DLL2 #define DLL_EXPORT extern "C" __declspec(dllexport) #else #define DLL_EXPORT extern "C" __declspec(dllimport) #endif DLL_EXPORT int MajorVersion(); DLL_EXPORT void RegisterInitCallback(const char* key, void(*callback)()); DLL_EXPORT void RegisterCallback(const char* key, void(*callback)()); DLL_EXPORT void Init();// dllmain2.cpp #include <windows.h> #include <map> #include <string> #define DLL_EXPORT_DLL2 #include "exports.h" /////////////////////////////////////////////////////////////////////////////// int MajorVersion() { return1; } /////////////////////////////////////////////////////////////////////////////// staticstd::map<std::string, void(*)()> s_init_callbacks; void RegisterInitCallback(const char* key, void(*callback)()) { s_init_callbacks.insert(std::make_pair(key, callback)); } /////////////////////////////////////////////////////////////////////////////// class MyGlobalVariable { public: MyGlobalVariable() { automodule = LoadLibrary(L"dll3.dll"); } }; MyGlobalVariable s_culprit; /////////////////////////////////////////////////////////////////////////////// staticstd::map<std::string, void(*)()> s_callbacks; void RegisterCallback(const char* key, void(*callback)()) { s_callbacks.insert(std::make_pair(key, callback)); } /////////////////////////////////////////////////////////////////////////////// void Init() { for (auto it : s_init_callbacks) { it.second(); } }dll3该模块会自动调用
dll2.dll导出的接口进行注册// dllmain3.cpp #include <windows.h> #include <iostream> #include "../common/autorunner.h" #include "../dll2/exports.h" /////////////////////////////////////////////////////////////////////////////// void Dll3InitCallback() { std::cout << "I'm callback from dll3.dll" << std::endl; } BEGIN_AUTO_RUN RegisterInitCallback("dll3", Dll3InitCallback); END_AUTO_RUN /////////////////////////////////////////////////////////////////////////////// void Dll3Callback() { std::cout << "I'm callback from dll3.dll" << std::endl; } BEGIN_AUTO_RUN RegisterCallback("dll3", Dll3Callback); END_AUTO_RUN
直接运行程序,从表面上看一切正常,但是在调试器下运行程序的时候会遇到一个意想不到的异常。
调试运行
打开windbg,选择需要执行的程序,确定后输入g 命令,目标程序会发生异常,自动中断到windbg 中。

在windbg 中输入kc 查看调用栈,输出结果摘录如下(为了方便查看,输出结果有所调整,注意<---- 的部分):
0:000> kc
# Call Site
00 dll2!std::_Tree<std::string,void (__cdecl*)(void)>::_Insert_nohint<...>()
01 dll2!std::_Tree<std::string,void (__cdecl*)(void)>::emplace<...>()
02 dll2!std::map<std::string,void (__cdecl*)(void)>::insert<...>()
03 dll2!RegisterCallback //<----
04 dll3!<lambda_7ce22ad9d321cf7c9be3c0faf7e37347>::operator()
05 dll3!<lambda_7ce22ad9d321cf7c9be3c0faf7e37347>::<lambda_invoker_cdecl>
06 dll3!AutoRunner::AutoRunner //<----
07 dll3!`dynamic initializer for's_auto_runner_23''
08 ucrtbased!_initterm
09 dll3!dllmain_crt_process_attach
0a dll3!dllmain_crt_dispatch
0b dll3!dllmain_dispatch
0c dll3!_DllMainCRTStartup
0d ntdll!LdrpCallInitRoutine
0e ntdll!LdrpInitializeNode
0f ntdll!LdrpInitializeGraphRecurse
10 ntdll!LdrpPrepareModuleForExecution
11 ntdll!LdrpLoadDllInternal
12 ntdll!LdrpLoadDll
13 ntdll!LdrLoadDll
14 KERNELBASE!LoadLibraryExW
15 dll2!MyGlobalVariable::MyGlobalVariable //<----
16 dll2!`dynamic initializer for's_culprit''
17 ucrtbased!_initterm
18 dll2!dllmain_crt_process_attach
19 dll2!dllmain_crt_dispatch
1a dll2!dllmain_dispatch
1b dll2!_DllMainCRTStartup
1c ntdll!LdrpCallInitRoutine
1d ntdll!LdrpInitializeNode
1e ntdll!LdrpInitializeGraphRecurse
1f ntdll!LdrpInitializeGraphRecurse
20 ntdll!LdrpPrepareModuleForExecution
21 ntdll!LdrpLoadDllInternal
22 ntdll!LdrpLoadDll
23 ntdll!LdrLoadDll
24 KERNELBASE!LoadLibraryExW
25 KERNELBASE!LoadLibraryExA
26 KERNELBASE!LoadLibraryA
27 LoadDlls!LoadPlugins //<----
28 LoadDlls!main
29 LoadDlls!invoke_main
2a LoadDlls!__scrt_common_main_seh
2b LoadDlls!__scrt_common_main
2c LoadDlls!mainCRTStartup
2d KERNEL32!BaseThreadInitThunk
2e ntdll!RtlUserThreadStart在windbg 中输入.frame 0x27 切换到0x27 栈帧,然后输入dv 查看局部变量,可以发现确实是在加载dll1.dll。
0:000> .frame 0x27
27 000000d5`3a4ff610 00007ff7`da7debb7 LoadDlls!LoadPlugins+0xb3 [D:\MyBlogStuff\LoadDlls\src\LoadDlls\LoadDlls.cpp @ 19]
0:000> dv
module = 0xcccccccc`cccccccc
plugin = 0x00007ff7`da7ea740 "dll1.dll"
idx = 0n0
plugins = 0x000000d5`3a4ff868
result = { size=0x0 }结合代码可以整理整个执行流程,大概是这样的:
主程序
LoadDlls.exe会通过LoadPlugins()调用LoadLibrary()来加载dll1.dll,由于dll1.dll隐式依赖了dll2.dll,所以dll1.dll加载的时候会自动加载dll2.dll。dll2.dll中的全局变量s_culprit的构造函数(栈帧0x15)内部会调用LoadLibrary()加载dll3.dll(栈帧0x14)dll3.dll中的全局变量s_auto_runner_23的构造函数(栈帧0x6)内部会调用dll2.dll的导出函数RegisterCallback()(栈帧0x3)RegisterCallback()内部会调用s_callbacks.insert()把注册的回调函数保存起来,但是在保存过程中遇到了异常,中断到了windbg中。
查看异常
根据windbg 给出的提示,可以发现是在读取地址0x00000008 的时候发生了异常,此地址明显是不可访问的。
00007ffd 57684ff1 488b4008 mov rax,qword ptr [rax+8] ds:00000000 00000008=????????????????
看上去非常像是空指针异常。这段代码是在调用 s_callbacks.insert() 的时候执行的,大概率是s_callbacks 出了问题,在windbg 中使用dx s_callbacks -r4 查看 s_callbacks 的值,如下图:

可以发现,s_callbacks 中的值很奇怪,都是空值。看上去很像还没有初始化的样子。
结合上面整理的调用流程,可以发现是在调用dll2!s_culprit 的构造函数时接触发了对dll2!RegisterCallback() 的调用,这时dll2!s_callbacks 这个全局变量还没有初始化。
因为初始化完dll2!s_culprit,才会初始化dll2!s_callbacks。
至此,可以破案了。只需要调整一下这两个全局变量的顺序,问题就解决了。修改后的代码如下:
//MyGlobalVariable s_culprit; // 移动到 s_callbacks 下面
///////////////////////////////////////////////////////////////////////////////
static std::map<std::string, void(*)()> s_callbacks;
MyGlobalVariable s_culprit;亲自动手
相关工程代码已经上传到github 了,感兴趣的小伙伴儿可以下载验证。
总结
本次故障是因为在dll2.dll 的全局变量s_culprit 的构造函数中使用LoadLibrary() 加载了dll3.dll,而dll3.dll 中的全局变量构造函数会调用dll2!RegisterCallback(),这个函数内部会使用未初始化的全局变量dll2!s_callbacks。因为此时正在初始化dll2!s_culprit 的过程中,dll2!s_culprit 初始化完成后才会初始化dll2!s_callbacks。
相较于之前的案例,这次的案例更复杂,涉及到了多个模块。单看每个模块,问题都不大,但是放到一起就触发了这个异常。
所以,尽量不要在全局变量的构造函数中做复杂的工作,尤其要避免类似LoadLibrary 的操作。
参考资料
https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices
彩蛋
其实,这个问题背后还有一个更隐蔽的bug,不知道你是否看出来了呢?stay tuned!
967

被折叠的 条评论
为什么被折叠?



