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

缘起

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

背景介绍

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

run-result

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

exports-info

使用 process explorer 观察 dll 加载状况,发现只加载了一个 dll,没发现另一个 dll函数

dll-load-info

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

上调试器

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

callstack-and-exception-info

从上图右侧部分,能够看到完整的调用栈。this

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

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

exception-address

从上图能够看出,确实是挂在了 008B7F34 movsx ecx,byte ptr [eax]。由于 eax 的值是 0xD,咱们须要查明 eax 的值为何是 0xD。相信不少小伙伴都知道,eax 用来保存函数调用的返回值。咱们能够把注意力集中到 0x008B7F2c 处的 Call 指令了,调用的是 _Isnil() 成员函数。debug

查看 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 ,以下:

watch-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: 0x8  char _Color;        // offset: 0xC  char _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 的值。

watch-this-content

咱们发现 _Parent 的值确实是 0。难道也像上次同样,是没初始化致使的?可是其它成员明明有值,跟上次的状况有些不一样。咱们须要进一步分析 this 值的来源。

继续深刻

查看调用栈,咱们发现,this 来自 CTest2 的构造函数里调用的 CObjectManager::GetMap(),这个函数是 Base.dll 的导出函数,返回了一个 GetMap() 中定义的静态变量 s_manager,应该不是初始化顺序的问题了,由于当咱们第一次调用 GetMap() 的时候,其内部定义的静态变量会被初始化。那还会是什么问题呢?

想在 vs 中观察下 s_manager 的值,试了几种方式,都不行。

watch-s_manager_type_in_vs

无奈,继续请 windbg 出场。

windbg 出场

打开 windbg,附加到进程,注意必定要勾选 Noninvasive 选项,由于目标进程正在被 vs 调试。

attach-noninvasive

若是没勾选 Noninvasive 选项,会报下图中的错误。

attach-already-being-debugged-process-failed-tip

成功附加后,咱们先经过 x Base!*GetMap* 查找到 GetMap 的地址,而后使用 u 004B5830 L20 查看对应的反汇编并查找 s_manager 的地址,发现对应的地址是 004c431c

find_s_manager_address_by_windbg

咱们不能直接 dt s_manager,可是能够 dt 004c431c

watch-s_manager_in_windbg

观察出问题的 map 对象。对比看下二者有什么不一样,以下图:

compare-two-map

注意看上图红色高亮部分,在 Base.dll 中的定义是带 _Myproxy 的,_Myhead 的偏移是 4,而在 Extension2.dll 中,并无 _Myproxy,天然而然的,_Myhead 的偏移是 0。这是两个不一样的 map 类型!

至此,问题已经明确了,s_manager 在两个模块眼中不同,注意观察上图中地址(黄色高亮部分)都是 0x004c431c。接下来的工做就是找出为何 s_managerBase.dllExtension2.dll 中不同。

追本溯源

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

watch-inherit

从上图可知:_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 node  size_type _Mysize;	// number of elements};复制代码

_Container_base 的定义以下(删除了无关信息):

#if _ITERATOR_DEBUG_LEVEL == 0typedef _Container_base0 _Container_base;#elsetypedef _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

find-the-culprit

发现,Extension2.vcxproj 中的 stdafx.h 中定义了 #define _ITERATOR_DEBUG_LEVEL 0。若是没有显式定义,该宏的值受 _HAS_ITERATOR_DEBUGGING 影响。通常在 Debug 下,_ITERATOR_DEBUG_LEVEL 的值是 2。能够参考yvals.h 中的定义,截图以下:

iterator_debug_level

至此,咱们搞清了整个事情的前因后果。总结一下:

因为两个工程的 _ITERATOR_DEBUG_LEVEL 不同,致使 map 的根基类( _Container_base )不同,从而致使了两个工程眼中的 map 不同,尤为是 _Myhead 的偏移不同。间接致使了全局变量 g_t2 在初始化时崩溃,进而致使了对应的 dll 加载失败。

动手实战

强烈建议你也动手实战一番,毕竟纸上来的终觉浅。若是你也想动手实战,能够直接下载我保存好的转储文件和对应的调试符号,直接使用 windbg 分析。

dump 文件和对应的符号文件下载连接:

百度云连接: pan.baidu.com/s/1EkOVoevZ… 提取码: xui4

CSDN:download.csdn.net/download/xi…

也能够下载完整的工程文件,使用 vs2013 编译运行便可。若是没装 vs2013,也能够手动改为其它版本的 vs

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

百度云连接: pan.baidu.com/s/1swaTU-7G… 提取码: iwkj

CSDN:download.csdn.net/download/xi…

总结

  • 不要混用 DebugRelease 生成的 Dll

  • map 的基类会根据 _HAS_ITERATOR_DEBUGGING 的不一样而不一样。

  • 若是一个进程已经被调试了,咱们能够经过 Noninvasive 的方式附加到被调试的进程中,执行一些观察操做。

参考资料

相关文章
相关标签/搜索