调试实战 —— 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争锋篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我们是如何做DevOps的?
- 下一篇: Beetlex.Redis之Stream