最近項目里老是遇到 dll
加載不上的問題,緣由各類各樣。今天先總結一個雖然不是項目中實際遇到的問題,可是卻很是經典的問題。其它幾種問題,後續慢慢總結。html
示例代碼包含一個 exe
工程,兩個 dll
工程。 exe
會加載兩個 dll
並調用它們的導出函數(GetCallCount
),結果只有一個 dll
的導出函數被成功調用。會是什麼緣由呢?node
運行效果以下圖:bash
經過 dumpbin
已經確認兩個 dll
都有名爲 GetCallCount
的函數。可是隻有一個調用成功了,另一個卻調用失敗。函數
使用 process explorer
觀察 dll
加載狀況,發現只加載了一個 dll
,沒發現另一個 dll
。測試
對於這個問題,若是咱們使用 process monitor
觀察整個加載過程,看到的都是 Success
。以下圖:ui
說明,加載正常,在本地找到了這個文件,並正確的映射到內存空間中了。但爲何在進程中觀察不到這個 dll
呢?是時候上調試器了。this
直接在 vs
中按 F5
啓動,果真中斷到 vs
中了。spa
從上圖右側部分,咱們能夠看到完整的調用棧。.net
這裏簡單介紹下相關代碼。在 GlobalVariableInitializeOrder.cpp
的第 15
行調用了 HMODULE hDll2 = LoadLibraryA("GlobalVariableInitializeOrderDll2.dll");
加載對應的模塊。debug
Common\Test2.cpp
的第 10
行定義了全局變量 CTest2 g_t2;
(在 dll
中),問題就出在這個全局變量的初始化代碼中。
從上圖左側部分,咱們能夠得知錯誤代碼是 0xc0000005
,內存訪問異常。訪問的地址是 0x00000004
,對應的指令位置是 0x001EA6DB
。
從上圖中的反彙編看,確實是掛在了 001EA6DB mov eax,dword ptr [eax]
。由於 eax
的值是 4
,咱們須要查明 eax
爲何的值是 4
。相信不少小夥伴都知道,eax
用來保存函數調用的返回值。咱們能夠把注意力集中到 0x001EA6D6
處的 call
指令了,調用的是成員函數 _Root()
。
查看 vs
提供的源碼,以下:
_Nodeptr& _Root() const{ // return root of nonmutable tree return (this->_Parent(this->_Myhead));}複製代碼 |
咱們能夠發現 _Root()
內部簡單的調用了 _Parent()
函數,並把 this->_Myhead
看成參數傳遞過去了。再查看下 _Parent()
函數的源碼,以下:
static _Nodepref _Parent(_Nodeptr _Pnode){ // return reference to parent pointer in node return ((_Nodepref)_Pnode->_Parent);}複製代碼 |
務必注意: _Parent()
的返回值類型是 _Nodepref
,返回的是引用(最後三個字母 ref
已經說明了一切)!至關於返回的是 _Pnode->_Parent
的地址!咱們能夠查看 _Nodepref
的定義:typedef _Nodeptr& _Nodepref;
。
因此 _Root()
函數至關於 &(this->_Myhead->_Parent)
。咱們來觀察下 this
各個成員的值。
能夠看到 _Myhead
的值是 0
,類型是 std::_Tree_node<...>
。
咱們再看下 _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
的定義可知, _Parent
的偏移是 4
(由於是 32
位的程序,若是是 64
位,那麼是 8
)。
綜上,地址 001EA6D6
處的 call
指令反回了 4
。接下來的兩條指令是把返回值賦給局部變量 _Nodeptr _Pnode
。可是在執行第一條彙編指令 mov eax,dword ptr [eax]
時就掛了,由於 eax
的值是 4
,正常狀況下訪問 0x00000004
處的值固然會掛掉了。
至此,咱們知道了崩潰的直接緣由——訪問非法地址。可是根本緣由是什麼呢?爲何 _Myhead
是 0
呢? 我猜想是由於 map
尚未初始化。可是該如何證明這個猜想呢?
CTest2
的構造函數裏調用的是 CTest1::GetMap()
,GetMap()
內部會返回 CTest1
的靜態變量 static std::map<std::string, std::string> s_manager;
的引用。
若是能證實在 CTest2::g_t2
初始化時,CTest1::s_manager
還沒初始化,那麼咱們就證明了咱們的猜想。
我想到兩個辦法:
map
的構造函數中輸出一條日誌。在調用 g_t2
的構造函數時,查看是否有咱們在 map
中新加的日誌。第一種方法比較簡單,直接修改 vs
提供的源碼便可,注意修改只讀屬性。本文以第 2
種方法爲例展開。
本小節根據上面的調用棧簡單的介紹全局變量的初始化過程(只介紹咱們關心的部分)。
不知道各位小夥伴兒是否記得上面的調用棧。切換到 8
號棧幀,以下圖:
能夠發現,在 __DllMainCRTStartup()
函數中,當 dwReason == DLL_PROCESS_ATTACH
或者 dwReason == DLL_THREAD_ATTACH
的時候,會調用 _CRT_INIT()
函數。_CRT_INIT()
會執行運行時庫的初始化相關功能,好比,初始化全局變量。而後纔會調用用戶提供的 DllMain()
函數。
繼續切換到 7
號棧幀,以下圖:
經過註釋可知,_initterm()
是在調用 C++ constructors
。
咱們繼續切換到 6
號棧幀,以下圖:
根據註釋猜想,應該是在依次調用每一個全局變量的初始化函數。pfbegin
指向了保存全局變量初始化函數的表格的起始位置,pfend
指向最後一個有效位置的下一個位置,跟標準庫中的容器多麼類似啊。若是 *pfbegin
的值不爲 0
,說明表格對應的位置有有效的初始化函數,須要調用,不然就跳過。
在 vs
中,咱們想遍歷出這個表格的內容有些費勁。是時候請 windbg
出場了。
在使用 windbg
以前必定要設置好符號路徑,不然不少內容看不到。
使用 windbg
打開要運行的程序,在命令窗口輸入 bm GlobalVariableInitializeOrderDll2!_CRT_INIT
,埋伏好斷點後執行 g
命令繼續運行。
很快,就中斷到咱們設置好的斷點處了。在調用 _initterm()
的地方設置好斷點,執行 g
命令(也能夠和 vs
同樣按 F5
),斷下來後,單步進入 _initterm()
函數,執行 dv
查看局部變量。
從輸出結果可知,pfbegin = 0x001f6000
,pfend = 0x001f6250
。而後咱們就能夠用強悍的 dps
來查看pfbegin
和 pfend
之間的內容了。在命令窗口執行,dps 0x001f6000 0x001f6250
。由於有不少空項,這裏只截取中間部分。
咱們能夠很明顯的看到,g_t2
的構造函數在前,s_manager
的構造函數在後。
至此,已經證明了咱們以前的猜測。
由於工程 GlobalVariableInitializeOrderDll1
和工程 GlobalVariableInitializeOrderDll2
代碼如出一轍,只有一點點的不一樣,就是這一點不一樣致使了一個 dll
能夠正常使用,另一個卻不能正常使用。
咱們能夠用相同的手法觀察 GlobalVariableInitializeOrderDll1.dll
的初始化過程。
在命令窗口輸入 bm GlobalVariableInitializeOrderDll1!_CRT_INIT;g
,埋伏好斷點後運行起來。再次中斷後,使用相同的辦法進入_initterm()
函數,經過 dv
命令獲得 pfbegin = 0x10026000
和 pfend = 0x10026250
的值,而後執行 dps 0x10026000 0x10026250
,以下圖(一樣有不少空項,只截取了中間部分):
咱們發現,s_manager
的構造函數在前,g_t2
的構造函數在後。
咱們應該從根本上消除對全局變量的依賴,只須要把 s_manager
放到 GetMap()
中就能夠了。
static std::map<std::string, std::string>& GetMap(){ static std::map<std::string, std::string> s_manager; return s_manager;}複製代碼 |
但有時候,因爲各類各樣的緣由,咱們不能消除這種依賴。咱們還能夠調整全局變量的初始化順序。只要有辦法讓 g_t2
在 s_manager
以後再初始化就能夠了。對比兩個 dll
工程文件,咱們發現有一處關鍵的不一樣點。
在能正常加載的 dll
對應的工程中, Test1.cpp, Test2.cpp
出現的順序是 Test1.cpp, Test2.cpp
,在不能正常加載的 dll
對應的工程中,出現的順序是 Test2.cpp, Test1.cpp
。調整 dll2.vcxproj
中的文件順序和 dll1.vcxproj
同樣,再次編譯運行,一切順利。
強烈建議你也動手實戰一番,畢竟紙上來的終覺淺。若是你也想動手實戰,能夠下載完整的工程文件,使用 vs2013
編譯運行便可。若是沒裝 vs2013
,也能夠手動改爲其它版本的 vs
。
完整的測試工程下載連接:
百度雲 連接: pan.baidu.com/s/1gW1dZsNY… 提取碼: 7irh
CSDN 連接:download.csdn.net/download/xi…
永遠不要讓一個全局變量依賴另一個全局變量。
全局變量是在 DllMain
或者 main
函數執行前進行初始化的。
在 32
位程序中,通常使用 eax
保存函數的返回值。
dps
命令能夠按地址遍歷給定範圍的內容。
dv
命令能夠查看局部變量和參數。
若是有小夥伴兒對全局變量初始化感興趣,能夠參考如下幾篇文檔: