我是靠谱客的博主 开朗荔枝,最近开发中收集的这篇文章主要介绍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

最后

以上就是开朗荔枝为你收集整理的dll 调用exe_调试实战 —— dll 加载失败之 Debug Release争锋篇的全部内容,希望文章能够帮你解决dll 调用exe_调试实战 —— dll 加载失败之 Debug Release争锋篇所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(34)

评论列表共有 0 条评论

立即
投稿
返回
顶部