日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

调试实战 —— dll 加载失败之全局变量初始化篇

發布時間:2023/12/4 编程问答 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 调试实战 —— dll 加载失败之全局变量初始化篇 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

最近項目里總是遇到 dll 加載不上的問題,原因各種各樣。今天先總結一個雖然不是項目中實際遇到的問題,但是卻非常經典的問題。其它幾種問題,后續慢慢總結。

示例代碼包含一個 exe 工程,兩個 dll 工程。exe 會加載兩個 dll 并調用它們的導出函數(GetCallCount),結果只有一個 dll 的導出函數被成功調用。會是什么原因呢?

現象

運行效果如下圖:

run_result

通過 dumpbin 已經確認兩個 dll 都有名為 GetCallCount 的函數。但是只有一個調用成功了,另外一個卻調用失敗。

dumpbin-exports

使用 process explorer 觀察 dll 加載情況,發現只加載了一個 dll,沒發現另外一個 dll。

loaded_dll

對于這個問題,如果我們使用 procmon 觀察整個加載過程,看到的都是 Success。如下圖:

procmon-trace

說明,加載正常,在本地找到了這個文件,并正確的映射到內存空間中了。但為什么在進程中觀察不到這個 dll 呢?是時候上調試器了。

上調試器

直接在 vs 中按 F5 啟動,果然中斷到 vs 中了。

exception-and-call-stack

從上圖右側部分,我們可以看到完整的調用棧。

這里簡單介紹下相關代碼。在 GlobalVariableInitializeOrder.cpp 的第 15 行調用了 HMODULE hDll2 = LoadLibraryA("GlobalVariableInitializeOrderDll2.dll"); 加載對應的模塊。

Common\Test2.cpp 的第 10 行定義了全局變量 CTest2 g_t2;,問題就出在這個全局變量的初始化代碼中。

從上圖左側部分,我們可以得知錯誤代碼是 0xc0000005,內存訪問異常。訪問的地址是 0x00000004,對應的指令位置是 0x001EA6DB。

invalid-eax

從上圖中的反匯編看,確實是掛在了 001EA6DB mov eax,dword ptr [eax]。因為 eax 的值是 4,我們需要查明 eax 為什么的值是 4。相信很多小伙伴都知道,eax 用來保存函數調用的返回值。我們可以把注意力集中到 0x001EA6D6 處的 call 指令了,調用的是成員函數 _Root() 。

查看 vs 提供的源碼,如下:

_Nodeptr&?_Root()?const {?//?return?root?of?nonmutable?treereturn?(this->_Parent(this->_Myhead)); }

我們可以發現 _Root() 內部簡單的調用了 _Parent() 函數,并把 this->_Myhead 當作參數傳遞過去了。再查看下 _Parent() 函數的源碼,如下:

static?_Nodepref?_Parent(_Nodeptr?_Pnode) {?//?return?reference?to?parent?pointer?in?nodereturn?((_Nodepref)_Pnode->_Parent); }

務必注意: _Parent() 的返回值類型是 _Nodepref,返回的是引用(最后三個字母 ref 已經說明了一切)!相當于返回的是 _Pnode->_Parent 的地址!我們可以查看 _Nodepref 的定義:typedef _Nodeptr& _Nodepref;。

所以 _Root() 函數相當于 &(this->_Myhead->_Parent)。我們來觀察下 this 的值。

watch-this-value

可以看到 _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:?0x8char?_Color;????????//?offset:?0xCchar?_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-call-_CRT_INIT

    可以發現,在 __DllMainCRTStartup() 函數中,當 dwReason == DLL_PROCESS_ATTACH 或者 dwReason == DLL_THREAD_ATTACH 的時候,會調用 _CRT_INIT() 函數。_CRT_INIT() 會執行運行時庫的初始化相關功能,比如,初始化全局變量。然后才會調用用戶提供的 DllMain() 函數。

    繼續切換到 7 號棧幀,如下圖:

    crt_init

    通過注釋可知,_initterm() 是在調用 C++ constructors。

    我們繼續切換到 6 號棧幀,如下圖:

    _initterm

    根據注釋猜測,應該是在依次調用每個全局變量的初始化函數。pfbegin 指向了保存全局變量初始化函數的表格的起始位置,pfend 指向最后一個有效位置的下一個位置,跟標準庫中的容器多么相似啊。如果 *pfbegin 的值不為 0,說明表格對應的位置有有效的初始化函數,需要調用,否則就跳過。

    在 vs 中,我們想遍歷出這個表格的內容有些費勁。是時候請 windbg 出場了。

    windbg 出場

    在使用 windbg 之前一定要設置好符號路徑,否則很多內容看不到。

    使用 windbg 打開要運行的程序,在命令窗口輸入 bm GlobalVariableInitializeOrderDll2!_CRT_INIT ,埋伏好斷點后執行 g 命令繼續運行。

    set-breakpoint-by-bm

    很快,就中斷到我們設置好的斷點處了。在調用 _initterm() 的地方設置好斷點,執行 g 命令(也可以和 vs 一樣按 F5),斷下來后,單步進入 _initterm() 函數,執行 dv 查看局部變量。

    single-step-to-initterm

    從輸出結果可知,pfbegin = 0x001f6000,pfend = 0x001f6250。然后我們就可以用強悍的 dps 來查看pfbegin 和 pfend 之間的內容了。在命令窗口執行,dps 0x001f6000 0x001f6250。因為有很多空項,這里只截取中間部分。

    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 工程文件,我們發現有一處關鍵的不同點。

    difference-of-project1-2

    在能正常加載的 dll 對應的工程中, Test1.cpp, Test2.cpp 出現的順序是 Test1.cpp, Test2.cpp,在不能正常加載的 dll 對應的工程中,出現的順序是 Test2.cpp, Test1.cpp。調整 dll2.vcxproj 中的文件順序和 dll1.vcxproj 一樣,再次編譯運行,一切順利。

    success

    動手實戰

    強烈建議你也動手實戰一番,畢竟紙上來的終覺淺。如果你也想動手實戰,可以下載完整的工程文件,使用 vs2013 編譯運行即可。如果沒裝 vs2013,也可以手動改成其它版本的 vs。

    完整的測試工程下載鏈接:

    百度云 鏈接: https://pan.baidu.com/s/1gW1dZsNYZoo0s_rfaO2Jzg 提取碼: 7irh

    CSDN 鏈接:https://download.csdn.net/download/xiaoyanilw/12405380

    總結

    • 永遠不要讓一個全局變量依賴另外一個全局變量。

    • 全局變量是在 DllMain 或者 main 函數執行前進行初始化的。

    • 在?32?位程序中,一般使用?eax?保存函數的返回值。

    • dps 命令可以按地址遍歷給定范圍的內容。

    • dv 命令可以查看局部變量和參數。

    參考資料

    如果有小伙伴兒對全局變量初始化感興趣,可以參考以下幾篇文檔:

    https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?redirectedfrom=MSDN&view=vs-2019

    http://www.cppblog.com/xlshcn/archive/2007/12/07/37088.html

    http://bytepointer.com/resources/pietrek_libctiny_2001.htm

    需要你的

    總結

    以上是生活随笔為你收集整理的调试实战 —— dll 加载失败之全局变量初始化篇的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。