前言

最近项目里总是遇到 dll 加载不上的问题,原因各种各样。今天先总结一个虽然不是项目中实际遇到的问题,但是却非常经典的问题。其它几种问题,后续慢慢总结。

示例代码包含一个 exe 工程,两个 dll 工程。exe 会加载两个 dll 并调用它们的导出函数(GetCallCount),结果只有一个 dll 的导出函数被成功调用。会是什么原因呢?

现象

运行效果如下图:

run_result

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

dumpbin-exports

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

loaded_dll

对于这个问题,如果我们使用 procmon 观察整个加载过程,看到的都是 Success。如下图:

procmon-trace

说明,加载正常,在本地找到了这个文件,并正确的映射到内存空间中了。但为什么在进程中观察不到这个 dll 呢?是时候上调试器了。

上调试器

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

exception-and-call-stack

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

这里简单介绍下相关代码。在 GlobalVariableInitializeOrder.cpp 的第 15 行调用了 HMODULE hDll2 = LoadLibraryA("GlobalVariableInitializeOrderDll2.dll"); 加载对应的模块。

Common\Test2.cpp 的第 10 行定义了全局变量 CTest2 g_t2;,问题就出在这个全局变量的初始化代码中。

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

invalid-eax

从上图中的反汇编看,确实是挂在了 001EA6DB mov eax,dword ptr [eax]。因为 eax 的值是 4,我们需要查明 eax 为什么的值是 4。相信很多小伙伴都知道,eax 用来保存函数调用的返回值。我们可以把注意力集中到 0x001EA6D6 处的 call 指令了,调用的是成员函数 _Root()

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

_Nodeptr& _Root() const
{ // return root of nonmutable treereturn (this->_Parent(this->_Myhead));
}

我们可以发现 _Root() 内部简单的调用了 _Parent() 函数,并把 this->_Myhead 当作参数传递过去了。再查看下 _Parent() 函数的源码,如下:

static _Nodepref _Parent(_Nodeptr _Pnode)
{ // return reference to parent pointer in nodereturn ((_Nodepref)_Pnode->_Parent);
}

务必注意: _Parent() 的返回值类型是 _Nodepref,返回的是引用(最后三个字母 ref 已经说明了一切)!相当于返回的是 _Pnode->_Parent 的地址!我们可以查看 _Nodepref 的定义:typedef _Nodeptr& _Nodepref;

所以 _Root() 函数相当于 &(this->_Myhead->_Parent)。我们来观察下 this 的值。

watch-this-value

可以看到 _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: 0x8char _Color;        // offset: 0xCchar _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 处的值当然会挂掉了。

至此,我们知道了崩溃的直接原因——访问非法地址。但是根本原因是什么呢?为什么 _Myhead0 呢?我猜测是因为 map 还没有初始化。但是该如何证实这个猜测呢?

继续深入

CTest2 的构造函数里调用的是 CTest1::GetMap()GetMap() 内部会返回 CTest1 的静态变量 static std::map<std::string, std::string> s_manager; 的引用。

如果能证明在 CTest2::g_t2 初始化时,CTest1::s_manager 还没初始化,那么我们就证实了我们的猜测。

我想到两个办法:

  1. 在  map 的构造函数中输出一条日志。在调用 g_t2 的构造函数时,查看是否有我们在 map 中新加的日志。

  2. 明确每个全局变量的初始化顺序。

第一种方法比较简单,直接修改 vs 提供的源码即可,注意修改只读属性。本文以第 2 种方法为例展开。

全局变量初始化简介

本小节根据上面的调用栈简单的介绍全局变量的初始化过程(只介绍我们关心的部分)。

不知道各位小伙伴儿是否记得上面的调用栈。切换到 8 号栈帧,如下图:

__DllMainCRTStartup-call-_CRT_INIT

可以发现,在 __DllMainCRTStartup() 函数中,当 dwReason == DLL_PROCESS_ATTACH 或者 dwReason == DLL_THREAD_ATTACH 的时候,会调用 _CRT_INIT() 函数。_CRT_INIT() 会执行运行时库的初始化相关功能,比如,初始化全局变量。然后才会调用用户提供的 DllMain() 函数。

继续切换到 7 号栈帧,如下图:

crt_init

通过注释可知,_initterm() 是在调用 C++ constructors

我们继续切换到 6 号栈帧,如下图:

_initterm

根据注释猜测,应该是在依次调用每个全局变量的初始化函数。pfbegin 指向了保存全局变量初始化函数的表格的起始位置,pfend 指向最后一个有效位置的下一个位置,跟标准库中的容器多么相似啊。如果 *pfbegin 的值不为 0,说明表格对应的位置有有效的初始化函数,需要调用,否则就跳过。

vs 中,我们想遍历出这个表格的内容有些费劲。是时候请 windbg 出场了。

windbg 出场

在使用 windbg 之前一定要设置好符号路径,否则很多内容看不到。

使用 windbg 打开要运行的程序,在命令窗口输入 bm GlobalVariableInitializeOrderDll2!_CRT_INIT ,埋伏好断点后执行 g 命令继续运行。

set-breakpoint-by-bm

很快,就中断到我们设置好的断点处了。在调用 _initterm() 的地方设置好断点,执行 g 命令(也可以和 vs 一样按 F5),断下来后,单步进入 _initterm() 函数,执行 dv 查看局部变量。

single-step-to-initterm

从输出结果可知,pfbegin = 0x001f6000pfend = 0x001f6250。然后我们就可以用强悍的 dps 来查看pfbeginpfend 之间的内容了。在命令窗口执行,dps 0x001f6000 0x001f6250。因为有很多空项,这里只截取中间部分。

dps-0x001f6000-0x001f6250

我们可以很明显的看到,g_t2的构造函数在前,s_manager 的构造函数在后。

至此,已经证实了我们之前的猜想。

对比强化

因为工程 GlobalVariableInitializeOrderDll1 和工程 GlobalVariableInitializeOrderDll2 代码一模一样,只有一点点的不同,就是这一点不同导致了一个 dll 可以正常使用,另外一个却不能正常使用。

我们可以用相同的手法观察 GlobalVariableInitializeOrderDll1.dll 的初始化过程。

在命令窗口输入 bm GlobalVariableInitializeOrderDll1!_CRT_INIT;g ,埋伏好断点后运行起来。再次中断后,使用相同的办法进入_initterm() 函数,通过 dv 命令得到 pfbegin = 0x10026000pfend = 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_t2s_manager 之后再初始化就可以了。对比两个 dll 工程文件,我们发现有一处关键的不同点。

difference-of-project1-2

在能正常加载的 dll 对应的工程中, Test1.cpp, Test2.cpp 出现的顺序是 Test1.cpp, Test2.cpp,在不能正常加载的 dll 对应的工程中,出现的顺序是 Test2.cpp, Test1.cpp。调整 dll2.vcxproj 中的文件顺序和 dll1.vcxproj 一样,再次编译运行,一切顺利。

success

动手实战

强烈建议你也动手实战一番,毕竟纸上来的终觉浅。如果你也想动手实战,可以下载完整的工程文件,使用 vs2013 编译运行即可。如果没装 vs2013,也可以手动改成其它版本的 vs

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

百度云 链接: https://pan.baidu.com/s/1gW1dZsNYZoo0s_rfaO2Jzg 提取码: 7irh

CSDN 链接:https://download.csdn.net/download/xiaoyanilw/12405380

总结

  • 永远不要让一个全局变量依赖另外一个全局变量。

  • 全局变量是在 DllMain 或者 main 函数执行前进行初始化的。

  • 在 32 位程序中,一般使用 eax 保存函数的返回值。

  • dps 命令可以按地址遍历给定范围的内容。

  • dv 命令可以查看局部变量和参数。

参考资料

如果有小伙伴儿对全局变量初始化感兴趣,可以参考以下几篇文档:

https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?redirectedfrom=MSDN&view=vs-2019

http://www.cppblog.com/xlshcn/archive/2007/12/07/37088.html

http://bytepointer.com/resources/pietrek_libctiny_2001.htm

需要你的

调试实战 —— dll 加载失败之全局变量初始化篇相关推荐

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

    缘起 最近,项目里遇到一个 dll 加载不上的问题.实际项目比较复杂,但是解决后,又是这么的简单,合情合理.本文是我使用示例工程模拟的,实际项目中另有玄机,但问题的本质是一样的.本文从行文上与 < ...

  2. 模块“XXX.dll”加载失败

    具体问题:模块"XXX.dll"加载失败 请确保该二进制存储在指定的路径中,或者调试它以检查该二进制或相关的.DLL文件是否有问题  找不到指定的模块. 1.在安装C++软件的时候 ...

  3. initpki.dll加载失败 找不到指定的模块的解决办法

    有用户在更新Win10系统时,收到提示"模块'initpki.dll'加载失败.请确保该二进制存储在指定的路径中,或者调试它以检查该二进制或相关的DLL文件是否有问题.找不到指定的程序.&q ...

  4. python 调用 tensorflow.dll_python tensorflow导入DLL加载失败

    我安装了最新的python 3.6.4 x64版本 然后使用pip3为cpu-only安装tensorflow C:\>pip3 install tensorflow 但是当我尝试在python ...

  5. 电脑重装系统Win10“initpki.dll”加载失败怎么办?

    ​最近有非常多的小伙伴在使用电脑的时候会被提示initpki.dll模块加载失败或者找不到在指定的模块情况,这导致我们无法正常是去使用,那么遇到这种问题应该如何去解决呢? 更多电脑重装系统教程尽在小白 ...

  6. 计算机中丢失swr.dll,win10系统提示模块initpki.dll加载失败如何解决

    有不少用户在使用电脑的过程中,发现出现了模块initpki.dll加载失败的提示,遇到这样的问题该怎么办呢,本教程就给大家讲解一下 1.打开搜索,输入:powershell ,在windows pow ...

  7. 计算机中丢失swr.dll,initpki.dll加载失败找不到指定的模块0x80004005错误代码怎么办win10...

    如今越来越多的小伙伴都已经装上了win10系统,使用过程中难免也会碰到各种故障,例如有不少 具体步骤如下: 1.打开搜索,输入:powershell ,在windows powershell 上单击右 ...

  8. 计算机中丢失swr.dll,win10电脑中模块initpki.dll加载失败提示0x80004005错误代码如何解决...

    有不少win10系统用户反映说碰到这样一个故障,就是模块initpki.dll加载失败,并提示0x80004005错误代码,该怎么解决呢,接下来就随系统城小编一起来看看具体的操作步骤吧. 1.打开搜索 ...

  9. win10 电脑中模块initpki.dll加载失败提示0x80004005错误代码如何解决

    win10 电脑中模块initpki.dll加载失败提示0x80004005错误代码如何解决 有不少win10系统用户反映说碰到这样一个故障,就是模块initpki.dll加载失败,并提示0x8000 ...

最新文章

  1. 在数量上超过微型计算机的是什么,在数量上超过微型计算机的是什么计算机
  2. soj1209- 最短的距离(精度问题)
  3. sougou ubuntu 优麒麟_优麒麟(Ubuntu Kylin)17.04 正式版及银河麒麟社区版发布
  4. 安装 Flex2packagebeta_1.994
  5. Javascript 笔记与总结(2-6)var
  6. 暑期训练日志----2018.7.30
  7. Telnet命令在Linux / Unix中的用法
  8. Go语言核心之美 2.2-浮点数
  9. MongoDB数据库练习题
  10. 计算机局域网地址设置方法,怎么设置局域网电脑的ip地址和DNS?
  11. C++——EOF是个什么东西?
  12. win10下自动运行python程序脚本
  13. 硬币找钱问题(最小硬币和问题)详解与代码实现
  14. 全国大学的ftp及部分免费ftp地址
  15. 立体仓库货物识别率99.9%!AI让仓储管理事半功倍
  16. esx linux 硬盘 扩容,ESX虚拟机添加新磁盘并扩容逻辑卷
  17. DB2 的REORG_学习(2)_表重组
  18. c语言传奇引擎源码,Ei3.0服务端引擎源代码+登陆网关+客户端插件
  19. 快播“洗白”,难道要抛弃用户?
  20. JAVA -- stateless4j StateMachine 使用浅析(一)

热门文章

  1. nginx汇总(z)
  2. Java的LockSupport.park()实现分析
  3. 持续集成之配置TeamCity
  4. jQuery Pagination Ajax分页插件中文详解
  5. 深圳dotnet俱乐部新群
  6. 设置Windows 10时如何创建本地帐户
  7. 获取outlook 会议_如何仅在Microsoft Outlook中仅获取您关注的电子邮件的通知
  8. macbook 下载时睡眠_MacBook进入睡眠状态时如何自动使其静音
  9. 程序员笔记(知识)管理的一点经验
  10. Oracle 12C CDB、PDB常用管理命令