dll 调用exe_调试实战 —— dll 加载失败之 Debug Release争锋篇

31524bc3e89bd558dea5f9b1826e38a8.png

缘起

最近,项目里遇到一个 dll 加载不上的问题。实际项目比较复杂,但是解决后,又是这么的简单,合情合理。本文是我使用示例工程模拟的,实际项目中另有玄机,但问题的本质是一样的。本文从行文上与 《调试实战 —— dll 加载失败之全局变量初始化篇》 非常相似,示例代码也非常相似(原谅我比较懒),感兴趣的小伙伴儿可以对比来读。

背景介绍

示例代码中一共有四个工程,一个 exe,三个 dll。其中,Base.vcxproj 是封装了公共接口的工程,会生成 Base.dllExtension1.vcxprojExtension2.vcxproj 非常相似,会分别生成 Extension1.dllExtension2.dllMixConfiguration.vcxproj 会生成 MixConfiguration.exe ,该 exe 会加载 Extension1.dllExtension2.dll ,并调用它们的导出函数(象征性的调用)。程序运行起来后,发现只有一个 dll 的功能正常,另外一个 dll 的功能执行不正常。如下图:

c7babd0e83550e319d2b8f6509eab82a.png

通过 dumpbin 已经确认两个 dll 都有名为 GetCallCount 的函数。但是只有一个调用成功了,另外一个却调用失败。

f66683d7e4a67281c51c81805c670b93.png

使用 process explorer 观察 dll 加载情况,发现只加载了一个 dll,没发现另外一个 dll

73f32be7c37540b2ceefdc53bd034de4.png

与上一个问题一样,如果我们用 procmon 观察整个加载过程,看到的都是 Success。这里不截图了。直接上调试器。

上调试器

直接在 vs 中按 F5 启动,果然中断到 vs 中了。

761e21dd460c77f417e1d7d874594794.png

从上图右侧部分,我们可以看到完整的调用栈。

简单介绍下相关代码。在 MixConfigurationEntry.cpp 的第 15 行调用了auto hDll2 = LoadLibraryA("Extension2.dll"); 加载对应的模块。在 Extension2Extension2.cpp 的第 22 行定义了全局变量 CTest2 g_t2,问题就出在这个全局变量的初始化代码中。

从上图左侧部分,我们可以得知错误代码是 0xc0000005,内存访问异常。访问的地址是 0x0000000D,对应的指令地址是 008B7F34

5ec9282fac835ea02646684aed290c38.png

从上图可以看出,确实是挂在了 008B7F34 movsx ecx,byte ptr [eax]。因为 eax 的值是 0xD,我们需要查明 eax 的值为什么是 0xD。相信很多小伙伴都知道,eax 用来保存函数调用的返回值。我们可以把注意力集中到 0x008B7F2c 处的 Call 指令了,调用的是 _Isnil() 成员函数。

查看 vs 提供的源码,如下:

static char& _Isnil(_Nodeptr _Pnode)
{// return reference to nil flag in node
 return ((char&)_Pnode->_Isnil);
}

发现 _Isnil 内部简单的返回了 _Pnode_Isnil 成员。

务必注意: 这里返回的是 char&,返回的是引用!相当于返回的是 _Pnode->_Isnil 的地址!

我们可以在 Watch 窗口查看传递给 _Isnil() 的参数 _Pnode ,如下:

06010a9e18228191a60767ae3f206697.png

可以看到 _Pnode 的值是 0,类型是 std::_Tree_node<...>

我们查看下 std::_Tree_node 的定义,如下:

template<class _Value_type, class _Voidptr>
struct _Tree_node
{
 _Voidptr _Left;     // offset: 0x0
 _Voidptr _Parent;   // offset: 0x4
 _Voidptr _Right;    // offset: 0x8
 char _Color;        // offset: 0xC
 char _Isnil;        // offset: 0xD
 _Value_type _Myval; // offset: 0x10

private:
 _Tree_node& operator=(const _Tree_node&);
};

_Tree_node 的定义可知, _Isnil 的偏移是 0xD (一般,32 位的程序指针占 4 字节,如果是 64 位,那么占 8 字节)。

综上,地址 008B7F2C 处的 call 指令反回 0xD 合情合理。008B7F34 处的指令 movsx ecx,byte ptr [eax] 把返回值保存到 ecx 处,但是因为 eax 的值是 0xD,正常情况下访问 0x0000000D 处的值当然会挂掉了。

至此,我们知道了崩溃的直接原因——访问非法地址。但是根本原因是什么呢?为什么 _Pnode0 呢?

_Pnode 的值来自 _Nodeptr _Pnode = _Root();。根据《调试实战 —— dll 加载失败之全局变量初始化篇》 分析的结果, _Root() 函数相当于 &(this->_Myhead->_Parent)。赋值给 _Pnode 后,_Pnode 的值等于 this->_Myhead->_Parent 的值。我们需要观察下 this 的值。

3500317e5efbd92f8f5a2c05f0396568.png

我们发现 _Parent 的值确实是 0。难道也像上次一样,是没初始化导致的?但是其它成员明明有值,跟上次的情况有些不同。我们需要进一步分析 this 值的来源。

继续深入

查看调用栈,我们发现,this 来自 CTest2 的构造函数里调用的 CObjectManager::GetMap(),这个函数是 Base.dll 的导出函数,返回了一个 GetMap() 中定义的静态变量 s_manager,应该不是初始化顺序的问题了,因为当我们第一次调用 GetMap() 的时候,其内部定义的静态变量会被初始化。那还会是什么问题呢?

想在 vs 中观察下 s_manager 的值,试了几种方式,都不行。

f9fc500df0cdf12bd50309d1a2bcadb9.png

无奈,继续请 windbg 出场。

windbg 出场

打开 windbg,附加到进程,注意一定要勾选 Noninvasive 选项,因为目标进程正在被 vs 调试。

544c143690853f220eee92c952fa98c7.png

如果没勾选 Noninvasive 选项,会报下图中的错误。

7a111ea92d2e06ad5141463928b73a3e.png

成功附加后,我们先通过 x Base!*GetMap* 查找到 GetMap 的地址,然后使用 u 004B5830 L20 查看对应的反汇编并查找 s_manager 的地址,发现对应的地址是 004c431c

cfc66ec0646ed32da5baf5247eb320c7.png

我们不能直接 dt s_manager,但是可以 dt 004c431c

4b2675460c44576fcb7e13ec2e016df9.png

观察出问题的 map 对象。对比看下两者有什么不同,如下图:

d1c0fe088157bfbaa3fc7acbc07b3ea0.png

注意看上图红色高亮部分,在 Base.dll 中的定义是带 _Myproxy 的,_Myhead 的偏移是 4,而在 Extension2.dll 中,并没有 _Myproxy,自然而然的,_Myhead 的偏移是 0。这是两个不同的 map 类型!

至此,问题已经明确了,s_manager 在两个模块眼中不一样,注意观察上图中地址(黄色高亮部分)都是 0x004c431c。接下来的工作就是找出为什么 s_managerBase.dllExtension2.dll 中不一样。

追本溯源

我们知道, map 继承自 _Tree

vs 中观察继承关系,如下图:

2ea849ad86f3e14b91967b527e0d779a.png

从上图可知:_Tree 继承自 _Tree_compTree_comp 继承自 _Tree_buy_Tree_buy 继承自 _Tree_alloc_Tree_alloc 又继承自 _Tree_val_Tree_val 又继承自 _Container_base

这里我们需要关注 _Tree_val_Container_base

_Tree_val 定义如下(删除了无关信息):

template<class _Val_types>
class _Tree_val : public _Container_base
{
public:
 typedef typename _Val_types::_Nodeptr _Nodeptr;

    // remove unrelated typedefs and member functions

 _Nodeptr _Myhead; // pointer to head node
 size_type _Mysize; // number of elements
};

_Container_base 的定义如下(删除了无关信息):

#if _ITERATOR_DEBUG_LEVEL == 0
typedef _Container_base0 _Container_base;
#else
typedef _Container_base12 _Container_base;
#endif

可以发现,如果 _ITERATOR_DEBUG_LEVEL0_Container_base 就等价于 _Container_base0。否则 _Container_base 等价于 _Container_base12

继续观察_Container_base0_Container_base12 的定义。

_Container_base0 的定义如下:

struct _CRTIMP2_PURE _Container_base0
{
 void _Orphan_all() {}
 void _Swap_all(_Container_base0&) {}
};

_Container_base12 的定义如下(删除了无关的成员函数):

struct _CRTIMP2_PURE _Container_base12
{
public:
 // remove unrelated member functions
 _Container_proxy *_Myproxy;
};

也就是说,_ITERATOR_DEBUG_LEVEL 不同的时候,map 占用的内存是不一样的。我在项目中遇到的正是这个问题。

水落石出

知道 _ITERATOR_DEBUG_LEVEL 会导致 map 的内存结构不一样,我们还需要进一步查找是哪里导致了 _ITERATOR_DEBUG_LEVEL 的值不一样。在整个解决方案搜索 _ITERATOR_DEBUG_LEVEL

ca8a6b0a47f75d3c11ee04913d7c14b0.png

发现,Extension2.vcxproj 中的 stdafx.h 中定义了 #define _ITERATOR_DEBUG_LEVEL 0。如果没有显式定义,该宏的值受 _HAS_ITERATOR_DEBUGGING 影响。一般在 Debug 下,_ITERATOR_DEBUG_LEVEL 的值是 2。可以参考yvals.h 中的定义,截图如下:

c3bd1e3dc77b51a92567496765478a0e.png

至此,我们搞清了整个事情的来龙去脉。总结一下:

由于两个工程的 _ITERATOR_DEBUG_LEVEL 不一样,导致 map 的根基类( _Container_base )不一样,从而导致了两个工程眼中的 map 不一样,尤其是 _Myhead 的偏移不一样。间接导致了全局变量 g_t2 在初始化时崩溃,进而导致了对应的 dll 加载失败。

动手实战

强烈建议你也动手实战一番,毕竟纸上来的终觉浅。如果你也想动手实战,可以直接下载我保存好的转储文件和对应的调试符号,直接使用 windbg 分析。

dump 文件和对应的符号文件下载链接:

百度云链接: https://pan.baidu.com/s/1EkOVoevZWTHCQOBxZxmJ4w 提取码: xui4

CSDN:https://download.csdn.net/download/xiaoyanilw/12502717

也可以下载完整的工程文件,使用 vs2013 编译运行即可。如果没装 vs2013,也可以手动改成其它版本的 vs

完整的测试工程下载链接:

百度云链接: https://pan.baidu.com/s/1swaTU-7GiVHzdeWroWma6g 提取码: iwkj

CSDN:https://download.csdn.net/download/xiaoyanilw/12502953

总结

  • 不要混用 DebugRelease 生成的 Dll
  • map 的基类会根据 _HAS_ITERATOR_DEBUGGING 的不同而不同。

参考资料

  • vs2013 自带的 stl 源码
  • https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?redirectedfrom=MSDN&view=vs-2019
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当导入`_multiarray_umath`时出现DLL加载失败的错误,可能有几种解决方法: 1. 检查依赖项:确保您的环境中有正确的依赖项。有时,缺少其他库或组件可能导致DLL加载失败。您可以查看相关文档或官方网站以获取正确的依赖项列表,并确保已正确安装和配置它们。 2. 更新库版本:尝试更新您使用的库版本。有时,旧版本的库可能不兼容,并且可能导致DLL加载失败的问题。请查看库的官方网站或文档,以获取最新版本,并尝试更新。 3. 检查路径设置:确保库所在的路径正确设置。如果库的路径没有正确设置,系统将无法找到所需的DLL文件。您可以尝试将库所在的路径添加到系统的环境变量中,或者使用适当的方式指定路径。 4. 检查操作系统位数:确保您正在使用与库兼容的操作系统位数。如果您的操作系统是64位的,但是您正在尝试加载32位的DLL文件,可能会导致加载失败。确保使用与您的操作系统匹配的库版本。 5. 重新安装库:如果上述方法都无法解决问题,您可以尝试重新安装相关库。这可能涉及卸载当前安装的库,并使用最新版本进行重新安装。 注意:这些解决方法是一般性建议,并不保证一定能解决问题。根据具体情况,您可能需要进一步调查和尝试其他方法。如果问题仍然存在,您还可以查看相关库的错误日志或官方社区,以获取更多帮助和支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值