通过
dumpbin
已经确认两个
dll
都有名为
GetCallCount
的函数。但是只有一个调用成功了,另外一个却调用失败。
使用
process explorer
观察
dll
加载情况,发现只加载了一个
dll
,没发现另外一个
dll
。
对于这个问题,如果我们使用
process monitor
观察整个加载过程,看到的都是
Success
。如下图:
说明,加载正常,在本地找到了这个文件,并正确的映射到内存空间中了。但为什么在进程中观察不到这个
dll
呢?是时候上调试器了。
上调试器
直接在
vs
中按
F5
启动,果然中断到
vs
中了。
从上图右侧部分,我们可以看到完整的调用栈。
这里简单介绍下相关代码。在
GlobalVariableInitializeOrder.cpp
的第
15
行调用了
HMODULE hDll2 = LoadLibraryA("GlobalVariableInitializeOrderDll2.dll");
加载对应的模块。
Common\Test2.cpp
的第
10
行定义了全局变量
CTest2 g_t2;
(在
dll
中),问题就出在这个全局变量的初始化代码中。
从上图左侧部分,我们可以得知错误代码是
0xc0000005
,内存访问异常。访问的地址是
0x00000004
,对应的指令位置是
0x001EA6DB
。
从上图中的反汇编看,确实是挂在了
001EA6DB mov eax,dword ptr [eax]
。因为
eax
的值是
4
,我们需要查明
eax
为什么的值是
4
。相信很多小伙伴都知道,
eax
用来保存函数调用的返回值。我们可以把注意力集中到
0x001EA6D6
处的
call
指令了,调用的是成员函数
_Root()
。
查看
vs
提供的源码,如下:
_Nodeptr& _Root() const{ // return root of nonmutable tree return (this->_Parent(this->_Myhead));}复制代码
|
我们可以发现
_Root()
内部简单的调用了
_Parent()
函数,并把
this->_Myhead
当作参数传递过去了。再查看下
_Parent()
函数的源码,如下:
static _Nodepref _Parent(_Nodeptr _Pnode){ 复制代码
|
务必注意:
_Parent()
的返回值类型是
_Nodepref
,返回的是引用(最后三个字母
ref
已经说明了一切)!相当于返回的是
_Pnode->_Parent
的地址!我们可以查看
_Nodepref
的定义:
typedef _Nodeptr& _Nodepref;
。
所以
_Root()
函数相当于
&(this->_Myhead->_Parent)
。我们来观察下
this
各个成员的值。
可以看到
_Myhead
的值是
0
,类型是
std::_Tree_node<...>
。
我们再看下
_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
的定义可知,
_Parent
的偏移是
4
(因为是
32
位的程序,如果是
64
位,那么是
8
)。
综上,地址
001EA6D6
处的
call
指令反回了
4
。接下来的两条指令是把返回值赋给局部变量
_Nodeptr _Pnode
。但是在执行第一条汇编指令
mov eax,dword ptr [eax]
时就挂了,因为
eax
的值是
4
,正常情况下访问
0x00000004
处的值当然会挂掉了。
至此,我们知道了崩溃的直接原因——访问非法地址。但是根本原因是什么呢?为什么
_Myhead
是
0
呢? 我猜测是因为
map
还没有初始化。但是该如何证实这个猜测呢?
继续深入
CTest2
的构造函数里调用的是
CTest1::GetMap()
,
GetMap()
内部会返回
CTest1
的静态变量
static std::map<std::string, std::string> s_manager;
的引用。
如果能证明在
CTest2::g_t2
初始化时,
CTest1::s_manager
还没初始化,那么我们就证实了我们的猜测。
我想到两个办法:
-
在
map
的构造函数中输出一条日志。在调用
g_t2
的构造函数时,查看是否有我们在
map
中新加的日志。
-
明确每个全局变量的初始化顺序。
第一种方法比较简单,直接修改
vs
提供的源码即可,注意修改只读属性。本文以第
2
种方法为例展开。
全局变量初始化简介
本小节根据上面的调用栈简单的介绍全局变量的初始化过程(只介绍我们关心的部分)。
不知道各位小伙伴儿是否记得上面的调用栈。切换到
8
号栈帧,如下图:
可以发现,在
__DllMainCRTStartup()
函数中,当
dwReason == DLL_PROCESS_ATTACH
或者
dwReason == DLL_THREAD_ATTACH
的时候,会调用
_CRT_INIT()
函数。
_CRT_INIT()
会执行运行时库的初始化相关功能,比如,初始化全局变量。然后才会调用用户提供的
DllMain()
函数。
继续切换到
7
号栈帧,如下图:
通过注释可知,
_initterm()
是在调用
C++ constructors
。
我们继续切换到
6
号栈帧,如下图:
根据注释猜测,应该是在依次调用每个全局变量的初始化函数。
pfbegin
指向了保存全局变量初始化函数的表格的起始位置,
pfend
指向最后一个有效位置的下一个位置,跟标准库中的容器多么相似啊。如果
*pfbegin
的值不为
0
,说明表格对应的位置有有效的初始化函数,需要调用,否则就跳过。
在
vs
中,我们想遍历出这个表格的内容有些费劲。是时候请
windbg
出场了。
windbg 出场
在使用
windbg
之前一定要设置好符号路径,否则很多内容看不到。
使用
windbg
打开要运行的程序,在命令窗口输入
bm GlobalVariableInitializeOrderDll2!_CRT_INIT
,埋伏好断点后执行
g
命令继续运行。
很快,就中断到我们设置好的断点处了。在调用
_initterm()
的地方设置好断点,执行
g
命令(也可以和
vs
一样按
F5
),断下来后,单步进入
_initterm()
函数,执行
dv
查看局部变量。
从输出结果可知,
pfbegin = 0x001f6000
,
pfend = 0x001f6250
。然后我们就可以用强悍的
dps
来查看
pfbegin
和
pfend
之间的内容了。在命令窗口执行,
dps 0x001f6000 0x001f6250
。因为有很多空项,这里只截取中间部分。
我们可以很明显的看到,
g_t2
的构造函数在前,
s_manager
的构造函数在后。
至此,已经证实了我们之前的猜想。
对比强化
因为工程
GlobalVariableInitializeOrderDll1
和工程
GlobalVariableInitializeOrderDll2
代码一模一样,只有一点点的不同,就是这一点不同导致了一个
dll
可以正常使用,另外一个却不能正常使用。
我们可以用相同的手法观察
GlobalVariableInitializeOrderDll1.dll
的初始化过程。
在命令窗口输入
bm GlobalVariableInitializeOrderDll1!_CRT_INIT;g
,埋伏好断点后运行起来。再次中断后,使用相同的办法进入
_initterm()
函数,通过
dv
命令得到
pfbegin = 0x10026000
和
pfend = 0x10026250
的值,然后执行
dps 0x10026000 0x10026250
,如下图(同样有很多空项,只截取了中间部分):
我们发现,
s_manager
的构造函数在前,
g_t2
的构造函数在后。
修复
我们应该从根本上消除对全局变量的依赖,只需要把
s_manager
放到
GetMap()
中就可以了。
static std::map<std::string, std::string>& GetMap(){ static std::map<std::string, std::string> s_manager; return s_manager;}复制代码
|
但有时候,由于各种各样的原因,我们不能消除这种依赖。我们还可以调整全局变量的初始化顺序。只要有办法让
g_t2
在
s_manager
之后再初始化就可以了。对比两个
dll
工程文件,我们发现有一处关键的不同点。
在能正常加载的
dll
对应的工程中,
Test1.cpp, Test2.cpp
出现的顺序是
Test1.cpp, Test2.cpp
,在不能正常加载的
dll
对应的工程中,出现的顺序是
Test2.cpp, Test1.cpp
。调整
dll2.vcxproj
中的文件顺序和
dll1.vcxproj
一样,再次编译运行,一切顺利。
动手实战
强烈建议你也动手实战一番,毕竟纸上来的终觉浅。如果你也想动手实战,可以下载完整的工程文件,使用
vs2013
编译运行即可。如果没装
vs2013
,也可以手动改成其它版本的
vs
。
完整的测试工程下载链接:
百度云 链接:
pan.baidu.com/s/1gW1dZsNY…
提取码: 7irh
CSDN 链接:
download.csdn.net/download/xi…
总结