缘起

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

背景介绍

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

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

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

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

上调试器

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

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

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

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

从上图可以看出,确实是挂在了 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 nodereturn ((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: 0x8char _Color;        // offset: 0xCchar _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 处的值当然会挂掉了。

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

_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 出场

打开 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.dllExtension2.dll 中不一样。

追本溯源

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

从上图可知:_Tree 继承自 _Tree_compTree_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 nodesize_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

发现,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 文件和对应的符号文件下载链接:

百度云链接: 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 的不同而不同。

  • 如果一个进程已经被调试了,我们可以通过 Noninvasive 的方式附加到被调试的进程中,执行一些观察操作。

参考资料

  • vs2013 自带的 stl 源码

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

欢迎留言交流!

需要你的

调试实战 —— dll 加载失败之 Debug Release争锋篇相关推荐

  1. 调试实战 —— dll 加载失败之全局变量初始化篇

    前言 最近项目里总是遇到 dll 加载不上的问题,原因各种各样.今天先总结一个虽然不是项目中实际遇到的问题,但是却非常经典的问题.其它几种问题,后续慢慢总结. 示例代码包含一个 exe 工程,两个 d ...

  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. RPC 笔记(01)— RPC概念、调用流程、RPC 与 Restful API 区别
  2. Kibana:分析及可视化日志文件
  3. rabbitmq学习——安装测试
  4. tfs连不上团队资源管理器问题
  5. 2018年工业机器人销量排位_2020年工业机器人统计数据新鲜出炉
  6. eclipse ide for c/c++_大众CC新款开启新的人生 辉煌报价钜惠 _车讯网chexun.com
  7. 下载安装VS Code以及简单的配置使用
  8. ibm各种服务器visio图标vss文件,visio网络图标库vss大全
  9. 【云计算学习教程】什么是中间件?常见中间件有哪些?
  10. 【积】有向图中的louvain社区检测(二)
  11. 杉车网数据报告:2019年,新能源汽车渐入佳境
  12. 实现n*n乘法口诀表
  13. 物联网毕业设计 stm32远程智能浇花灌溉系统 - 单片机 嵌入式
  14. EasyUI(修改删除)
  15. php mysql oracle数据库表结构图_创建数据库表
  16. python多线程下载(荣耀)photo
  17. 如何基于无人机实现无线电点对点验证?
  18. 小程序的学习步骤计划
  19. 【循环测试试题】数字游戏
  20. 大专的我狂刷29天“阿里内部面试笔记”最终直接斩获十七个Offer

热门文章

  1. iOS duplicate symbol for architecture arm64 解决办法
  2. 页面闲置一段时间后,跳转
  3. UVA 10518 How Many Calls?
  4. 怎样去掉桌面图标和字的蓝色阴影
  5. head rush ajax chapter4 DOM
  6. 用计算机算算术平方根顺序是ON然后是什么,第2课时用计算器求一个正数的算术平方根.ppt...
  7. mysql的concat函数_MySQL中concat函数(连接字符串)
  8. 从Boxee的Amie Street访问音乐
  9. 初学者:如何使用虚拟PC将Windows 7安装到虚拟机
  10. 使用mintty(_如何使用Mintty改善Cygwin控制台