調試實戰 —— 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 的方式附加到被調試的進程中,執行一些觀察操做。

參考資料

相關文章
相關標籤/搜索