调试实战 —— dll 加载失败之 Debug Release争锋篇
缘起
最近,项目里遇到一个 dll
加载不上的问题。实际项目比较复杂,但是解决后,又是这么的简单,合情合理。本文是我使用示例工程模拟的,实际项目中另有玄机,但问题的本质是一样的。本文从行文上与 《调试实战 —— dll 加载失败之全局变量初始化篇》 非常相似,示例代码也非常相似(原谅我比较懒),感兴趣的小伙伴儿可以对比来读。
背景介绍
示例代码中一共有四个工程,一个 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
的功能执行不正常。如下图:
已经使用 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
处的值当然会挂掉了。
至此,我们知道了崩溃的直接原因——访问非法地址。但是根本原因是什么呢?为什么 _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 出场
打开 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 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_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
文件和对应的符号文件下载链接:
百度云链接: 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
总结
不要混用
Debug
和Release
生成的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争锋篇相关推荐
- 调试实战 —— dll 加载失败之全局变量初始化篇
前言 最近项目里总是遇到 dll 加载不上的问题,原因各种各样.今天先总结一个虽然不是项目中实际遇到的问题,但是却非常经典的问题.其它几种问题,后续慢慢总结. 示例代码包含一个 exe 工程,两个 d ...
- 模块“XXX.dll”加载失败
具体问题:模块"XXX.dll"加载失败 请确保该二进制存储在指定的路径中,或者调试它以检查该二进制或相关的.DLL文件是否有问题 找不到指定的模块. 1.在安装C++软件的时候 ...
- initpki.dll加载失败 找不到指定的模块的解决办法
有用户在更新Win10系统时,收到提示"模块'initpki.dll'加载失败.请确保该二进制存储在指定的路径中,或者调试它以检查该二进制或相关的DLL文件是否有问题.找不到指定的程序.&q ...
- python 调用 tensorflow.dll_python tensorflow导入DLL加载失败
我安装了最新的python 3.6.4 x64版本 然后使用pip3为cpu-only安装tensorflow C:\>pip3 install tensorflow 但是当我尝试在python ...
- 电脑重装系统Win10“initpki.dll”加载失败怎么办?
最近有非常多的小伙伴在使用电脑的时候会被提示initpki.dll模块加载失败或者找不到在指定的模块情况,这导致我们无法正常是去使用,那么遇到这种问题应该如何去解决呢? 更多电脑重装系统教程尽在小白 ...
- 计算机中丢失swr.dll,win10系统提示模块initpki.dll加载失败如何解决
有不少用户在使用电脑的过程中,发现出现了模块initpki.dll加载失败的提示,遇到这样的问题该怎么办呢,本教程就给大家讲解一下 1.打开搜索,输入:powershell ,在windows pow ...
- 计算机中丢失swr.dll,initpki.dll加载失败找不到指定的模块0x80004005错误代码怎么办win10...
如今越来越多的小伙伴都已经装上了win10系统,使用过程中难免也会碰到各种故障,例如有不少 具体步骤如下: 1.打开搜索,输入:powershell ,在windows powershell 上单击右 ...
- 计算机中丢失swr.dll,win10电脑中模块initpki.dll加载失败提示0x80004005错误代码如何解决...
有不少win10系统用户反映说碰到这样一个故障,就是模块initpki.dll加载失败,并提示0x80004005错误代码,该怎么解决呢,接下来就随系统城小编一起来看看具体的操作步骤吧. 1.打开搜索 ...
- win10 电脑中模块initpki.dll加载失败提示0x80004005错误代码如何解决
win10 电脑中模块initpki.dll加载失败提示0x80004005错误代码如何解决 有不少win10系统用户反映说碰到这样一个故障,就是模块initpki.dll加载失败,并提示0x8000 ...
最新文章
- RPC 笔记(01)— RPC概念、调用流程、RPC 与 Restful API 区别
- Kibana:分析及可视化日志文件
- rabbitmq学习——安装测试
- tfs连不上团队资源管理器问题
- 2018年工业机器人销量排位_2020年工业机器人统计数据新鲜出炉
- eclipse ide for c/c++_大众CC新款开启新的人生 辉煌报价钜惠 _车讯网chexun.com
- 下载安装VS Code以及简单的配置使用
- ibm各种服务器visio图标vss文件,visio网络图标库vss大全
- 【云计算学习教程】什么是中间件?常见中间件有哪些?
- 【积】有向图中的louvain社区检测(二)
- 杉车网数据报告:2019年,新能源汽车渐入佳境
- 实现n*n乘法口诀表
- 物联网毕业设计 stm32远程智能浇花灌溉系统 - 单片机 嵌入式
- EasyUI(修改删除)
- php mysql oracle数据库表结构图_创建数据库表
- python多线程下载(荣耀)photo
- 如何基于无人机实现无线电点对点验证?
- 小程序的学习步骤计划
- 【循环测试试题】数字游戏
- 大专的我狂刷29天“阿里内部面试笔记”最终直接斩获十七个Offer
热门文章
- iOS duplicate symbol for architecture arm64 解决办法
- 页面闲置一段时间后,跳转
- UVA 10518 How Many Calls?
- 怎样去掉桌面图标和字的蓝色阴影
- head rush ajax chapter4 DOM
- 用计算机算算术平方根顺序是ON然后是什么,第2课时用计算器求一个正数的算术平方根.ppt...
- mysql的concat函数_MySQL中concat函数(连接字符串)
- 从Boxee的Amie Street访问音乐
- 初学者:如何使用虚拟PC将Windows 7安装到虚拟机
- 使用mintty(_如何使用Mintty改善Cygwin控制台