调试实战 —— dll 加载失败之 Debug Release争锋篇
緣起
最近,項(xiàng)目里遇到一個(gè) dll 加載不上的問(wèn)題。實(shí)際項(xiàng)目比較復(fù)雜,但是解決后,又是這么的簡(jiǎn)單,合情合理。本文是我使用示例工程模擬的,實(shí)際項(xiàng)目中另有玄機(jī),但問(wèn)題的本質(zhì)是一樣的。本文從行文上與 《調(diào)試實(shí)戰(zhàn) —— dll 加載失敗之全局變量初始化篇》 ?非常相似,示例代碼也非常相似(原諒我比較懶),感興趣的小伙伴兒可以對(duì)比來(lái)讀。
背景介紹
示例代碼中一共有四個(gè)工程,一個(gè) exe,三個(gè) dll。其中,Base.vcxproj 是封裝了公共接口的工程,會(huì)生成 Base.dll。Extension1.vcxproj 和 Extension2.vcxproj 非常相似,會(huì)分別生成 Extension1.dll 和 Extension2.dll。MixConfiguration.vcxproj 會(huì)生成 MixConfiguration.exe ,該 exe 會(huì)加載 Extension1.dll 和 Extension2.dll ,并調(diào)用它們的導(dǎo)出函數(shù)(象征性的調(diào)用)。程序運(yùn)行起來(lái)后,發(fā)現(xiàn)只有一個(gè) dll 的功能正常,另外一個(gè) dll 的功能執(zhí)行不正常。如下圖:
已經(jīng)使用?dumpbin 確認(rèn)兩個(gè) dll 都有名為 GetCallCount 的函數(shù)。但是只有一個(gè)調(diào)用成功了,另外一個(gè)卻調(diào)用失敗。
使用 process explorer 觀察 dll 加載情況,發(fā)現(xiàn)只加載了一個(gè) dll,沒(méi)發(fā)現(xiàn)另外一個(gè) dll。
與上一個(gè)問(wèn)題一樣,如果我們用 procmon 觀察整個(gè)加載過(guò)程,看到的都是 Success。這里不截圖了。直接上調(diào)試器。
上調(diào)試器
直接在 vs 中按 F5 啟動(dòng),果然中斷到 vs 中了。
從上圖右側(cè)部分,可以看到完整的調(diào)用棧。
簡(jiǎn)單介紹下相關(guān)代碼。在 MixConfiguration\Entry.cpp 的第 15 行調(diào)用了auto hDll2 = LoadLibraryA("Extension2.dll"); 加載對(duì)應(yīng)的模塊。在 Extension2\Extension2.cpp 的第 22 行定義了全局變量 CTest2 g_t2,問(wèn)題就出在這個(gè)全局變量的初始化代碼中。
從上圖左側(cè)部分可知,錯(cuò)誤代碼是 0xc0000005,內(nèi)存訪問(wèn)異常。訪問(wèn)的地址是 0x0000000D,對(duì)應(yīng)的指令地址是 008B7F34。
從上圖可以看出,確實(shí)是掛在了 008B7F34 movsx ecx,byte ptr [eax]。因?yàn)?eax 的值是 0xD,我們需要查明 eax 的值為什么是 0xD。相信很多小伙伴都知道,eax 用來(lái)保存函數(shù)調(diào)用的返回值。我們可以把注意力集中到 0x008B7F2c 處的 Call 指令了,調(diào)用的是 _Isnil() 成員函數(shù)。
查看 vs 提供的源碼,如下:
static?char&?_Isnil(_Nodeptr?_Pnode) {//?return?reference?to?nil?flag?in?nodereturn?((char&)_Pnode->_Isnil); }發(fā)現(xiàn) _Isnil 內(nèi)部簡(jiǎn)單的返回了 _Pnode 的 _Isnil 成員。
務(wù)必注意: 這里返回的是 char&,返回的是引用!相當(dāng)于返回的是 _Pnode->_Isnil 的地址!
在 Watch 窗口查看傳遞給 _Isnil() 的參數(shù) _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 字節(jié),如果是 64 位,那么占 8 字節(jié))。
綜上,地址 008B7F2C 處的 call 指令反回 0xD 合情合理。008B7F34 處的指令 movsx ecx,byte ptr [eax] 把返回值保存到 ecx 處,但是因?yàn)?eax 的值是 0xD,正常情況下訪問(wèn) 0x0000000D 處的值當(dāng)然會(huì)掛掉了。
至此,我們知道了崩潰的直接原因——訪問(wèn)非法地址。但是根本原因是什么呢?為什么 _Pnode 是 0 呢?
_Pnode 的值來(lái)自 _Nodeptr _Pnode = _Root();。根據(jù)《調(diào)試實(shí)戰(zhàn) —— dll 加載失敗之全局變量初始化篇》 分析的結(jié)果, _Root() 函數(shù)相當(dāng)于 &(this->_Myhead->_Parent)。賦值給 _Pnode 后,_Pnode 的值等于 this->_Myhead->_Parent 的值。我們需要觀察下 this 的值。
我們發(fā)現(xiàn) _Parent 的值確實(shí)是 0。難道也像上次一樣,是沒(méi)初始化導(dǎo)致的?但是其它成員明明有值,跟上次的情況有些不同。我們需要進(jìn)一步分析 this 值的來(lái)源。
繼續(xù)深入
查看調(diào)用棧,我們發(fā)現(xiàn),this 來(lái)自 CTest2 的構(gòu)造函數(shù)里調(diào)用的 CObjectManager::GetMap(),這個(gè)函數(shù)是 Base.dll 的導(dǎo)出函數(shù),返回了一個(gè) GetMap() 中定義的靜態(tài)變量 s_manager,應(yīng)該不是初始化順序的問(wèn)題了,因?yàn)楫?dāng)我們第一次調(diào)用 GetMap() 的時(shí)候,其內(nèi)部定義的靜態(tài)變量會(huì)被初始化。那還會(huì)是什么問(wèn)題呢?
想在 vs 中觀察下 s_manager 的值,試了幾種方式,都不行。
無(wú)奈,繼續(xù)請(qǐng) windbg 出場(chǎng)。
windbg 出場(chǎng)
打開(kāi) windbg,附加到進(jìn)程,注意一定要勾選 Noninvasive 選項(xiàng),因?yàn)槟繕?biāo)進(jìn)程正在被 vs 調(diào)試。
如果沒(méi)勾選 Noninvasive 選項(xiàng),會(huì)報(bào)下圖中的錯(cuò)誤。
成功附加后,我們先通過(guò) x Base!*GetMap* 查找到 GetMap 的地址,然后使用 u 004B5830 L20 查看對(duì)應(yīng)的反匯編并查找 s_manager 的地址,發(fā)現(xiàn)對(duì)應(yīng)的地址是 004c431c。
我們不能直接 dt s_manager,但是可以 dt 004c431c。
觀察出問(wèn)題的 map 對(duì)象。對(duì)比看下兩者有什么不同,如下圖:
注意看上圖紅色高亮部分,在 Base.dll 中的定義是帶 _Myproxy 的,_Myhead 的偏移是 4,而在 Extension2.dll 中,并沒(méi)有 _Myproxy,自然而然的,_Myhead 的偏移是 0。這是兩個(gè)不同的 map 類型!
至此,問(wèn)題已經(jīng)明確了,s_manager 在兩個(gè)模塊眼中不一樣,注意觀察上圖中地址(黃色高亮部分)都是 0x004c431c。接下來(lái)的工作就是找出為什么 s_manager ?在 Base.dll 和 Extension2.dll 中不一樣。
追本溯源
在 vs 中觀察繼承關(guān)系,如下圖:
從上圖可知:_Tree 繼承自 _Tree_comp,Tree_comp 繼承自 _Tree_buy, _Tree_buy 繼承自 _Tree_alloc,_Tree_alloc 又繼承自 _Tree_val, _Tree_val 又繼承自 _Container_base。而?map?繼承自?_Tree。
這里我們只需要關(guān)注 _Tree_val 和 _Container_base。
_Tree_val 定義如下(刪除了無(wú)關(guān)信息):
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 的定義如下(刪除了無(wú)關(guān)信息):
#if?_ITERATOR_DEBUG_LEVEL?==?0 typedef?_Container_base0?_Container_base; #else typedef?_Container_base12?_Container_base; #endif可以發(fā)現(xiàn),如果 _ITERATOR_DEBUG_LEVEL 是 0,_Container_base 就等價(jià)于 _Container_base0。否則 _Container_base ?等價(jià)于 _Container_base12。
繼續(xù)觀察_Container_base0 ?和 _Container_base12 的定義。
_Container_base0 的定義如下:
struct?_CRTIMP2_PURE?_Container_base0 {void?_Orphan_all()?{}void?_Swap_all(_Container_base0&)?{} };_Container_base12 的定義如下(刪除了無(wú)關(guān)的成員函數(shù)):
struct?_CRTIMP2_PURE?_Container_base12 { public://?remove?unrelated?member?functions_Container_proxy?*_Myproxy; };也就是說(shuō),_ITERATOR_DEBUG_LEVEL 不同的時(shí)候,map 占用的內(nèi)存是不一樣的。我在項(xiàng)目中遇到的正是這個(gè)問(wèn)題。
水落石出
知道 _ITERATOR_DEBUG_LEVEL 會(huì)導(dǎo)致 map 的內(nèi)存結(jié)構(gòu)不一樣,我們還需要進(jìn)一步查找是哪里導(dǎo)致了 _ITERATOR_DEBUG_LEVEL 的值不一樣。在整個(gè)解決方案搜索 _ITERATOR_DEBUG_LEVEL。
發(fā)現(xiàn),Extension2.vcxproj 中的 stdafx.h 中定義了 #define _ITERATOR_DEBUG_LEVEL 0。如果沒(méi)有顯式定義,該宏的值受 _HAS_ITERATOR_DEBUGGING 影響。一般在 Debug 下,_ITERATOR_DEBUG_LEVEL 的值是 2。可以參考yvals.h 中的定義,截圖如下:
至此,我們搞清了整個(gè)事情的來(lái)龍去脈。總結(jié)一下:
由于兩個(gè)工程的 _ITERATOR_DEBUG_LEVEL 不一樣,導(dǎo)致 map 的根基類( _Container_base )不一樣,從而導(dǎo)致了兩個(gè)工程眼中的 map 不一樣,尤其是 _Myhead 的偏移不一樣。間接導(dǎo)致了全局變量 g_t2 在初始化時(shí)崩潰,進(jìn)而導(dǎo)致了對(duì)應(yīng)的 dll 加載失敗。
動(dòng)手實(shí)戰(zhàn)
強(qiáng)烈建議你也動(dòng)手實(shí)戰(zhàn)一番,畢竟紙上來(lái)的終覺(jué)淺。如果你也想動(dòng)手實(shí)戰(zhàn),可以直接下載我保存好的轉(zhuǎn)儲(chǔ)文件和對(duì)應(yīng)的調(diào)試符號(hào),直接使用 windbg 分析。
dump 文件和對(duì)應(yīng)的符號(hào)文件下載鏈接:
百度云鏈接: https://pan.baidu.com/s/1EkOVoevZWTHCQOBxZxmJ4w 提取碼: xui4
CSDN:https://download.csdn.net/download/xiaoyanilw/12502717
也可以下載完整的工程文件,使用 vs2013 編譯運(yùn)行即可。如果沒(méi)裝 vs2013,也可以手動(dòng)改成其它版本的 vs。
完整的測(cè)試工程下載鏈接:
百度云鏈接: https://pan.baidu.com/s/1swaTU-7GiVHzdeWroWma6g 提取碼: iwkj
CSDN:https://download.csdn.net/download/xiaoyanilw/12502953
總結(jié)
不要混用 Debug 和 Release 生成的 Dll。
map 的基類會(huì)根據(jù) _HAS_ITERATOR_DEBUGGING 的不同而不同。
如果一個(gè)進(jìn)程已經(jīng)被調(diào)試了,我們可以通過(guò) Noninvasive 的方式附加到被調(diào)試的進(jìn)程中,執(zhí)行一些觀察操作。
參考資料
vs2013 自帶的 stl 源碼
https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?redirectedfrom=MSDN&view=vs-2019
歡迎留言交流!
需要你的
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的调试实战 —— dll 加载失败之 Debug Release争锋篇的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 我们是如何做DevOps的?
- 下一篇: 一文说通Dotnet Core的后台任务