最近,项目里遇到一个 dll
加载不上的问题。实际项目比较复杂,可是解决后,又是这么的简单,合情合理。本文是我使用示例工程模拟的,实际项目中另有玄机,但问题的本质是同样的。本文从行文上与 《调试实战 —— dll 加载失败之全局变量初始化篇》 很是类似,示例代码也很是类似(原谅我比较懒),感兴趣的小伙伴儿能够对比来读。node
示例代码中一共有四个工程,一个 exe
,三个 dll
。其中,Base.vcxproj
是封装了公共接口的工程,会生成 Base.dll
。Extension1.vcxproj
和 Extension2.vcxproj
很是类似,会分别生成 Extension1.dll
和 Extension2.dll
。MixConfiguration.vcxproj
会生成 MixConfiguration.exe
,该 exe
会加载 Extension1.dll
和 Extension2.dll
,并调用它们的导出函数(象征性的调用)。程序运行起来后,发现只有一个 dll
的功能正常,另一个 dll
的功能执行不正常。以下图:git
已经经过 dumpbin
确认两个 dll
都有名为 GetCallCount
的函数。可是只有一个调用成功了,另一个却调用失败。bash
使用 process explorer
观察 dll
加载状况,发现只加载了一个 dll
,没发现另一个 dll
。函数
与上一个问题同样,若是用 procmon
观察整个加载过程,看到的都是 Success
。这里不截图了。直接上调试器。测试
直接在 vs
中按 F5
启动,果真中断到 vs
中了。ui
从上图右侧部分,能够看到完整的调用栈。this
简单介绍下相关代码。在 MixConfiguration\Entry.cpp
的第 15
行调用了auto hDll2 = LoadLibraryA("Extension2.dll");
加载对应的模块。在 Extension2\Extension2.cpp
的第 22
行定义了全局变量 CTest2 g_t2
,问题就出在这个全局变量的初始化代码中。spa
从上图左侧部分可知,错误代码是 0xc0000005
,内存访问异常。访问的地址是 0x0000000D
,对应的指令地址是 008B7F34
。.net
从上图能够看出,确实是挂在了 008B7F34 movsx ecx,byte ptr [eax]
。由于 eax
的值是 0xD
,咱们须要查明 eax
的值为何是 0xD
。相信不少小伙伴都知道,eax
用来保存函数调用的返回值。咱们能够把注意力集中到 0x008B7F2c
处的 Call
指令了,调用的是 _Isnil()
成员函数。debug
查看 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
,以下:
能够看到 _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: 0x10private: _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
处的值固然会挂掉了。
至此,咱们知道了崩溃的直接缘由——访问非法地址。可是根本缘由是什么呢?为何 _Pnode
是 0
呢?
_Pnode
的值来自 _Nodeptr _Pnode = _Root();
。根据《调试实战 —— dll 加载失败之全局变量初始化篇》 分析的结果, _Root()
函数至关于 &(this->_Myhead->_Parent)
。赋值给 _Pnode
后,_Pnode
的值等于 this->_Myhead->_Parent
的值。咱们须要观察下 this
的值。
咱们发现 _Parent
的值确实是 0
。难道也像上次同样,是没初始化致使的?可是其它成员明明有值,跟上次的状况有些不一样。咱们须要进一步分析 this
值的来源。
查看调用栈,咱们发现,this
来自 CTest2
的构造函数里调用的 CObjectManager::GetMap()
,这个函数是 Base.dll
的导出函数,返回了一个 GetMap()
中定义的静态变量 s_manager
,应该不是初始化顺序的问题了,由于当咱们第一次调用 GetMap()
的时候,其内部定义的静态变量会被初始化。那还会是什么问题呢?
想在 vs
中观察下 s_manager
的值,试了几种方式,都不行。
无奈,继续请 windbg
出场。
打开 windbg
,附加到进程,注意必定要勾选 Noninvasive
选项,由于目标进程正在被 vs
调试。
若是没勾选 Noninvasive
选项,会报下图中的错误。
成功附加后,咱们先经过 x Base!*GetMap*
查找到 GetMap
的地址,而后使用 u 004B5830 L20
查看对应的反汇编并查找 s_manager
的地址,发现对应的地址是 004c431c
。
咱们不能直接 dt s_manager
,可是能够 dt 004c431c
。
观察出问题的 map
对象。对比看下二者有什么不一样,以下图:
注意看上图红色高亮部分,在 Base.dll
中的定义是带 _Myproxy
的,_Myhead
的偏移是 4
,而在 Extension2.dll
中,并无 _Myproxy
,天然而然的,_Myhead
的偏移是 0
。这是两个不一样的 map
类型!
至此,问题已经明确了,s_manager
在两个模块眼中不同,注意观察上图中地址(黄色高亮部分)都是 0x004c431c
。接下来的工做就是找出为何 s_manager
在 Base.dll
和 Extension2.dll
中不同。
在 vs
中观察继承关系,以下图:
从上图可知:_Tree
继承自 _Tree_comp
,Tree_comp
继承自 _Tree_buy
, _Tree_buy
继承自 _Tree_alloc
,_Tree_alloc
又继承自 _Tree_val
, _Tree_val
又继承自 _Container_base
。而 map
继承自 _Tree
。
这里咱们只须要关注 _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 == 0typedef _Container_base0 _Container_base;#elsetypedef _Container_base12 _Container_base;#endif复制代码 |
能够发现,若是 _ITERATOR_DEBUG_LEVEL
是 0
,_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
。
发现,Extension2.vcxproj
中的 stdafx.h
中定义了 #define _ITERATOR_DEBUG_LEVEL 0
。若是没有显式定义,该宏的值受 _HAS_ITERATOR_DEBUGGING
影响。通常在 Debug
下,_ITERATOR_DEBUG_LEVEL
的值是 2
。能够参考yvals.h
中的定义,截图以下:
至此,咱们搞清了整个事情的前因后果。总结一下:
因为两个工程的 _ITERATOR_DEBUG_LEVEL
不同,致使 map
的根基类( _Container_base
)不同,从而致使了两个工程眼中的 map
不同,尤为是 _Myhead
的偏移不同。间接致使了全局变量 g_t2
在初始化时崩溃,进而致使了对应的 dll
加载失败。
强烈建议你也动手实战一番,毕竟纸上来的终觉浅。若是你也想动手实战,能够直接下载我保存好的转储文件和对应的调试符号,直接使用 windbg
分析。
dump
文件和对应的符号文件下载连接:
百度云连接: pan.baidu.com/s/1EkOVoevZ… 提取码: xui4
CSDN:download.csdn.net/download/xi…
也能够下载完整的工程文件,使用 vs2013
编译运行便可。若是没装 vs2013
,也能够手动改为其它版本的 vs
。
完整的测试工程下载连接:
百度云连接: pan.baidu.com/s/1swaTU-7G… 提取码: iwkj
CSDN:download.csdn.net/download/xi…
不要混用 Debug
和 Release
生成的 Dll
。
map
的基类会根据 _HAS_ITERATOR_DEBUGGING
的不一样而不一样。
若是一个进程已经被调试了,咱们能够经过 Noninvasive
的方式附加到被调试的进程中,执行一些观察操做。
vs2013
自带的 stl
源码