缘起
在上篇文章《调试实战 | 调试另外一个由于全局变量初始化顺序导致的 dll 加载失败问题(上)》中,解决了由于全局变量初始化顺序不对导致的崩溃问题。但是代码里还有一处非常隐蔽的 bug,今天继续介绍一下这个问题及对应的解决方法。
示例程序
在上篇文章代码的基础上,修改 LoadDlls 工程中的 main() 函数代码,使其加载 dll1.dll 和 dll2.dll(上篇文章中只加载了 dll1.dll)。修改后的关键代码如下:
1 | int main() |
直接运行崩溃了
运行完 LoadPlugins() 后,点击任意按键继续运行,会继续执行 InitPlugins(),程序会在此函数中崩溃。因为我设置 procdump 为 JIT 调试器(具体设置方法可以参考这篇文章),程序崩溃后会自动调用 procdump 保存崩溃转储文件。
用 windbg 打开转储文件,无脑点击 !analyze -v,让 windbg 帮我们自动分析,分析结果如下图:

从上图中的调用栈基本可以确定在执行 dll3.dll 中的代码时发生了异常。
而且这次的异常不是因为读/写非法地址导致的,而是执行到非法地址导致的(注意红色高亮部分的提示 Attempt to execute non-executable address 00007ffbdd9812e4)。
可以猜测 00007ffbdd9812e4 是属于 dll3.dll 的(可以通过 !address 00007ffbdd9812e4 验证),而且当执行到 00007ffbdd9812e4 的时候,dll3.dll 已经被卸载了(注意底部红色高亮部分的 Unloaded 关键字)。
使用 !address 00007ffbdd9812e4 查看该地址的信息,如下图:

从上图可知,00007ffbdd9812e4 确实是属于 dll3.dll 的,但是 dll3.dll 已经被卸载了。00007ffbdd9812e4 所在的页面是 MEM_FREE 的,而且是 PAGE_NOACCESS 的。
但是,程序为什么会执行到一个已经被卸载的模块中的地址呢?
追本溯源
从调用栈可以得知,dll3.dll 中的函数是由 dll2!Init() 调用的, 通过查看 dll2!Init() 源码,可以发现 dll2!Init() 函数中会遍历 s_init_callbacks。在 windbg 中输入dx dll2!s_init_callbacks 查看其内容,如下图:

可以发现,s_init_callbacks 中只有一项,是 dll3.dll 中的函数。
根据源码可知,dll3.dll 在加载的时候会自动调用 dll2!RegisterInitCallback() 注册回调函数。
1 | BEGIN_AUTO_RUN |
可以推理,s_init_callbacks 中的函数是 dll3!Dll3InitCallback。
加载 dll3.dll 的调试符号后,查看调用栈,可以证实我们的猜想。如下图:

根据源码可知,dll3.dll 在加载的时候还会自动调用 RegisterCallback(),相关代码如下:
1 | BEGIN_AUTO_RUN |
因为调用 RegisterCallback() 的时候发生了异常(在上篇文章中已经分析过了),导致 dll3.dll 加载失败,dll3.dll 会被自动卸载 。但是通过 RegisterInitCallback() 注册的回调函数已经保存在了 dll2!s_init_callbacks 中,没有被清除。
当后面调用 dll2!Init() 时,会调用 dll3.dll 注册到 dll2!s_init_callbacks 中的函数(Dll3InitCallback),因为 dll3.dll 已经被卸载了,对应的函数地址也无效了,也就会发生上文中的异常。
深入思考
如果在 LoadDlls.exe 中也显式加载了 dll3.dll,还会不会崩溃呢?答案是可能崩溃,也可能不崩溃。
因为 RegisterInitCallback()内部更新数据时使用的是 map.insert(),这会导致一个问题 —— 如果 map 中已经存在相同的 key,那么 insert() 会失败,不会更新数据。
试想,如果显式加载 dll3.dll 成功,但是 dll3.dll 的基址变了。map 中保存的还是旧的无效地址,而不是新函数地址。
如果 dll3.dll 的基址没有发生变化,新函数地址与旧函数地址一样,程序可以非常“幸运”的正常运行。
要想规避这种问题,可以换一种写法,修改后的代码如下:
1 | void RegisterInitCallback(const char* key, void(*callback)()) |
总结
调试由于模块被卸载导致的异常,其实很简单 —— 直接在
windbg中使用!analyze -v基本上就可以定位到问题了这次的崩溃,直接原因在于模块被意外的卸载了,归根结底还是代码不规范导致的
如果
map中想保存最新数据,那么不要使用map.insert,而要使用operater []map.insert()在插入新数据时,如果发现对应的key已经存在,会直接返回false,而不会更新数据。operator []会强制更新数据