VC驿站

 找回密码
 加入驿站

QQ登录

只需一步,快速开始

搜索
查看: 158|回复: 4

[原创] 调试实战 —— dll 加载失败之 Debug Release 争锋篇

[复制链接]
70_avatar_middle
最佳答案
0 
在线会员 发表于 2020-6-21 22:09:57 | 显示全部楼层 |阅读模式
缘起最近,项目里遇到一个 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 的功能执行不正常。如下图:
调试实战 —— dll 加载失败之 Debug Release 争锋篇
已经通过 dumpbin 确认两个 dll 都有名为 GetCallCount 的函数。但是只有一个调用成功了,另外一个却调用失败。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
使用 process explorer 观察 dll 加载情况,发现只加载了一个 dll,没发现另外一个 dll。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
与上一个问题一样,如果用 procmon 观察整个加载过程,看到的都是 Success。这里不截图了。直接上调试器。
上调试器直接在 vs 中按 F5 启动,果然中断到 vs 中了。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
从上图右侧部分,可以看到完整的调用栈。
简单介绍下相关代码。在 MixConfiguration\Entry.cpp 的第 15 行调用了auto hDll2 = LoadLibraryA("Extension2.dll"); 加载对应的模块。在 Extension2\Extension2.cpp 的第 22 行定义了全局变量 CTest2 g_t2,问题就出在这个全局变量的初始化代码中。
从上图左侧部分可知,错误代码是 0xc0000005,内存访问异常。访问的地址是 0x0000000D,对应的指令地址是 008B7F34。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
从上图可以看出,确实是挂在了 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 node
  return ((char&)_Pnode->_Isnil);
}
发现 _Isnil 内部简单的返回了 _Pnode 的 _Isnil 成员。
务必注意: 这里返回的是 char&,返回的是引用!相当于返回的是 _Pnode->_Isnil 的地址!
可以在 Watch 窗口查看传递给 _Isnil() 的参数 _Pnode ,如下:
调试实战 —— dll 加载失败之 Debug Release 争锋篇
可以看到 _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: 0x10

private:
  _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 的值。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
我们发现 _Parent 的值确实是 0。难道也像上次一样,是没初始化导致的?但是其它成员明明有值,跟上次的情况有些不同。我们需要进一步分析 this 值的来源。
继续深入查看调用栈,我们发现,this 来自 CTest2 的构造函数里调用的 CObjectManager::GetMap(),这个函数是 Base.dll 的导出函数,返回了一个 GetMap() 中定义的静态变量 s_manager,应该不是初始化顺序的问题了,因为当我们第一次调用 GetMap() 的时候,其内部定义的静态变量会被初始化。那还会是什么问题呢?
想在 vs 中观察下 s_manager 的值,试了几种方式,都不行。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
无奈,继续请 windbg 出场。
windbg 出场打开 windbg,附加到进程,注意一定要勾选 Noninvasive 选项,因为目标进程正在被 vs 调试。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
如果没勾选 Noninvasive 选项,会报下图中的错误。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
成功附加后,我们先通过 x Base!*GetMap* 查找到 GetMap 的地址,然后使用 u 004B5830 L20 查看对应的反汇编并查找 s_manager 的地址,发现对应的地址是 004c431c。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
我们不能直接 dt s_manager,但是可以 dt 004c431c。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
观察出问题的 map 对象。对比看下两者有什么不同,如下图:
调试实战 —— dll 加载失败之 Debug Release 争锋篇
注意看上图红色高亮部分,在 Base.dll 中的定义是带 _Myproxy 的,_Myhead 的偏移是 4,而在 Extension2.dll 中,并没有 _Myproxy,自然而然的,_Myhead 的偏移是 0。这是两个不同的 map 类型!
至此,问题已经明确了,s_manager 在两个模块眼中不一样,注意观察上图中地址(黄色高亮部分)都是 0x004c431c。接下来的工作就是找出为什么 s_manager  在 Base.dll 和 Extension2.dll 中不一样。
追本溯源在 vs 中观察继承关系,如下图:
调试实战 —— dll 加载失败之 Debug Release 争锋篇
从上图可知:_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 == 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。
调试实战 —— dll 加载失败之 Debug Release 争锋篇
发现,Extension2.vcxproj 中的 stdafx.h 中定义了 #define _ITERATOR_DEBUG_LEVEL 0。如果没有显式定义,该宏的值受 _HAS_ITERATOR_DEBUGGING 影响。一般在 Debug 下,_ITERATOR_DEBUG_LEVEL 的值是 2。可以参考yvals.h 中的定义,截图如下:
调试实战 —— dll 加载失败之 Debug Release 争锋篇
至此,我们搞清了整个事情的来龙去脉。总结一下:
由于两个工程的 _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




上一篇:【分享】Windows PE学习资料
下一篇:visual 一些常见异常
07_avatar_middle
最佳答案
4 
在线会员 发表于 2020-6-22 08:04:28 | 显示全部楼层
大佬牛逼,学习了。
09_avatar_middle
最佳答案
0 
online_vip 发表于 2020-6-24 16:09:52 | 显示全部楼层
你好 谢谢分享  想问下 LoadLibraryA函数这里出现异常的话 该怎么拦截
09_avatar_middle
最佳答案
0 
online_vip 发表于 2020-6-28 20:49:00 | 显示全部楼层
你好 谢谢分享  想问下 LoadLibraryA函数这里出现异常的话 该怎么拦截
09_avatar_middle
最佳答案
0 
online_vip 发表于 2020-6-28 20:50:26 | 显示全部楼层
你好 谢谢分享  想问下 LoadLibraryA函数这里出现异常的话 该怎么拦截
您需要登录后才可以回帖 登录 | 加入驿站 qq_login

本版积分规则

×【发帖 友情提示】
1、请回复有意义的内容,请勿恶意灌水;
2、纯数字、字母、表情等无意义的内容系统将自动删除;
3、若正常回复后帖子被自动删除,为系统误删的情况,请重新回复其他正常内容或等待管理员审核通过后会自动发布;
4、感谢您对VC驿站一如既往的支持,谢谢合作!

关闭

站长提醒上一条 /2 下一条

QQ|小黑屋|手机版|VC驿站 ( 辽ICP备09019393号 )|网站地图wx_jqr

GMT+8, 2020-7-7 04:29

Powered by CcTry.CoM

© 2009-2020 cctry.com

快速回复 返回顶部 返回列表