调试实战 | 调试另外一个由于全局变量初始化顺序导致的 dll 加载失败问题(上)

缘起

最近又遇到了一个程序功能不正常的问题,深入调查后发现与全局变量初始化顺序有非常大的关系,只不过这次更加隐蔽。

之前总结了两篇与全局变量初始化顺序有关的文章,感兴趣的小伙伴儿可以参考《调试实战 | dll 加载失败之全局变量初始化篇》《调试实战 | 全局变量初始化顺序探究》

在排查错误之前先简单介绍一下相关代码。

示例程序

示例程序一共包含 4 个工程:LoadDlls, dll1, dll2, dll3

  • 主程序 LoadDlls.exe 会加载 dll1.dll

  • dll1.dll 隐式依赖了 dll2.dll,所以 dll1.dll 加载的时候会自动加载 dll2.dll

  • dll2.dll 中的全局变量 s_culprit 的构造函数会加载 dll3.dll

  • dll3.dll 加载的时候会自动调用 dll2.dll 的导出函数 RegisterInitCallback()RegisterCallback()

下面是每个工程的关键代码

  • src/common/autorunner.h

    该文件是公共头文件,实现了自动注册逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 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

    该工程只有一个源文件,用来模拟加载各种插件。对应的源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // 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)
    {
    const char* 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;

    const char* 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);
    return 0;
    }
  • dll1

    该工程非常简单,什么有用的事情都没做,但是会依赖 dll2,加载 dll1.dll 的时候会自动加载 dll2.dll

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 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_RUN
  • dll2

    该模块提供了注册回调函数的导出接口,并实现了回调逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// dllmain2.cpp

#include <windows.h>
#include <map>
#include <string>

#define DLL_EXPORT_DLL2
#include "exports.h"

///////////////////////////////////////////////////////////////////////////////
int MajorVersion() { return 1; }

///////////////////////////////////////////////////////////////////////////////
static std::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() { auto module = LoadLibrary(L"dll3.dll"); }
};

MyGlobalVariable s_culprit;

///////////////////////////////////////////////////////////////////////////////
static std::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 导出的接口进行注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 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 中。

exception-break-to-windbg

windbg 中输入 kc 查看调用栈,输出结果摘录如下(为了方便查看,输出结果有所调整,注意 <---- 的部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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

1
2
3
4
5
6
7
8
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 的值,如下图:

view-s_callbacks

可以发现,s_callbacks 中的值很奇怪,都是空值。看上去很像还没有初始化的样子。

结合上面整理的调用流程,可以发现是在调用 dll2!s_culprit 的构造函数时接触发了对 dll2!RegisterCallback() 的调用,这时 dll2!s_callbacks 这个全局变量还没有初始化。

因为初始化完 dll2!s_culprit,才会初始化 dll2!s_callbacks

至此,可以破案了。只需要调整一下这两个全局变量的顺序,问题就解决了。修改后的代码如下:

1
2
3
4
5
6
//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 的操作。

参考资料

彩蛋

其实,这个问题背后还有一个更隐蔽的 bug,不知道你是否看出来了呢?stay tuned!

BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%