最近,項目裏遇到一個 dll
加載不上的問題。實際項目比較複雜,可是解決後,又是這麼的簡單,合情合理。本文是我使用示例工程模擬的,實際項目中另有玄機,但問題的本質是同樣的。本文從行文上與 《調試實戰 —— dll 加載失敗之全局變量初始化篇》 很是類似,示例代碼也很是類似(原諒我比較懶),感興趣的小夥伴兒能夠對比來讀。node
示例代碼中一共有四個工程,一個 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
的功能執行不正常。以下圖:git
已經經過 dumpbin
確認兩個 dll
都有名爲 GetCallCount
的函數。可是隻有一個調用成功了,另一個卻調用失敗。bash
使用 process explorer
觀察 dll
加載狀況,發現只加載了一個 dll
,沒發現另一個 dll
。函數
與上一個問題同樣,若是用 procmon
觀察整個加載過程,看到的都是 Success
。這裏不截圖了。直接上調試器。測試
直接在 vs
中按 F5
啓動,果真中斷到 vs
中了。ui
從上圖右側部分,能夠看到完整的調用棧。this
簡單介紹下相關代碼。在 MixConfiguration\Entry.cpp
的第 15
行調用了auto hDll2 = LoadLibraryA("Extension2.dll");
加載對應的模塊。在 Extension2\Extension2.cpp
的第 22
行定義了全局變量 CTest2 g_t2
,問題就出在這個全局變量的初始化代碼中。spa
從上圖左側部分可知,錯誤代碼是 0xc0000005
,內存訪問異常。訪問的地址是 0x0000000D
,對應的指令地址是 008B7F34
。.net
從上圖能夠看出,確實是掛在了 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
,以下:
能夠看到 _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
處的值固然會掛掉了。
至此,咱們知道了崩潰的直接緣由——訪問非法地址。可是根本緣由是什麼呢?爲何 _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
,附加到進程,注意必定要勾選 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 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_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
文件和對應的符號文件下載連接:
百度雲連接: 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…
不要混用 Debug
和 Release
生成的 Dll
。
map
的基類會根據 _HAS_ITERATOR_DEBUGGING
的不一樣而不一樣。
若是一個進程已經被調試了,咱們能夠經過 Noninvasive
的方式附加到被調試的進程中,執行一些觀察操做。
vs2013
自帶的 stl
源碼