關於顯示載入動態連結庫模組及解除安裝的問題

青山見我應如是發表於2021-02-05

問題起因是,在一次模組解除安裝後,程式執行異常。遂對動態連結庫做一些測試。

動態庫載入方式有兩種,隱式載入和顯示載入,隱式載入包含xxx.lib匯入庫,在程式執行之前由動態載入器完成所有載入;顯示載入則使用LoadLibrary方式;具體資料可參考《程式設計師的自我修養:連結,裝載與庫》一書。

動態庫標頭檔案:

關於顯示載入動態連結庫模組及解除安裝的問題
 1 #ifdef DYNAMICLIBRARYTEST_EXPORTS
 2 #define DYNAMICLIBRARYTEST_API __declspec(dllexport)
 3 #else
 4 #define DYNAMICLIBRARYTEST_API __declspec(dllimport)
 5 #endif
 6 
 7 // 此類是從 dll 匯出的
 8 class DYNAMICLIBRARYTEST_API Base {
 9 public:
10     Base(void);
11     
12     virtual int* virtualFunc();
13     virtual ~Base();
14     
15 
16     int a = 8;
17     int b = 9;
18     char c[10] = {'H','e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd' };
19     // TODO: 在此處新增方法。
20 };
21 
22 class DYNAMICLIBRARYTEST_API Derive  : public Base
23 {
24 public:
25     Derive(void);
26     int* normalFunc()
27     {
28         return nullptr;
29     }
30 
31     int* virtualFunc() override;
32      ~Derive();
33     // TODO: 在此處新增方法。
34 };
35 
36 extern "C" DYNAMICLIBRARYTEST_API int i_global;
37 
38 extern "C" DYNAMICLIBRARYTEST_API double d_global;
39 
40 extern "C" DYNAMICLIBRARYTEST_API char c_global[6];
41 
42 extern "C" DYNAMICLIBRARYTEST_API int func1(void);
43 extern "C" DYNAMICLIBRARYTEST_API Derive* createDerive();
View Code

 動態庫實現檔案:

關於顯示載入動態連結庫模組及解除安裝的問題
 1 // DynamicLibraryTest.cpp : 定義 DLL 的匯出函式。
 2 //
 3 
 4 #include "DynamicLibraryTest.h"
 5 // 這是匯出變數的一個示例
 6 DYNAMICLIBRARYTEST_API int i_global = 1;
 7 int i_global_1 = 9;
 8 DYNAMICLIBRARYTEST_API double d_global = 2 ;
 9 DYNAMICLIBRARYTEST_API char c_global[6] = {'G', 'l','o', 'b', 'a', 'l'};
10 
11 // 這是匯出函式的一個示例。
12 DYNAMICLIBRARYTEST_API int func1(void)
13 {
14     return -1;
15 }
16 
17 Derive * createDerive()
18 {
19     return new Derive;
20 }
21 
22 Base::Base()
23 {
24     return;
25 }
26 
27 
28 int* Base::virtualFunc()
29 {
30     return nullptr;
31 }
32 
33 Base::~Base()
34 {
35 }
36 
37 Derive::Derive(void)
38 {
39 }
40 
41 int* Derive::virtualFunc()
42 {
43     int c = a + b;
44     c--;
45     return new int[10];
46 }
47 
48 Derive::~Derive()
49 {
50 }
View Code

檢視匯出符號:

 

 可以看到匯出的變數命名比較正常,這是因為是以C風格匯出的。不然就是C++的詭異風格修飾。

主程式實現:project.cpp

 1 // project.cpp : 此檔案包含 "main" 函式。程式執行將在此處開始並結束。
 2 //
 3 
 4 #include <iostream>
 5 #include "DynamicLibraryTest.h"
 6 #include <Windows.h>
 7 
 8 #define LIBNAME "C:/Users/Admin/source/repos/DynamicLibraryTest/Release/DLL_1.dll"
 9 
10 typedef int*(*NormalFunc)();
11 typedef Derive*(*CreateDerive)();
12 int main()
13 {
14     const char* szStr = LIBNAME;
15     WCHAR wszClassName[256]; 
16     memset(wszClassName, 0, sizeof(wszClassName));
17     MultiByteToWideChar(CP_ACP, 0, szStr, strlen(szStr) + 1, wszClassName, sizeof(wszClassName) / sizeof(wszClassName[0])); 
18     HMODULE hmodule = ::LoadLibrary(wszClassName);
19     if (NULL == hmodule) 
20     {
21         printf("LoadLibrary failed/n"); 
22         return -1; 
23     }
24 
25     CreateDerive funcDerive = (CreateDerive)GetProcAddress(hmodule, "createDerive");
26     NormalFunc nor = (NormalFunc)GetProcAddress(hmodule, "?normalFunc@Derive@@QAEPAHXZ");
27     Derive* d = funcDerive();//分配在堆上
28     Derive* d2 = funcDerive();
29     //d->normalFunc();//不能直接呼叫非虛擬函式
30     //本模組儲存了一份虛表地址在堆上,每次訪問虛擬函式,通過堆上的儲存的虛表地址查詢真正的虛表,
31     //而虛表儲存在對映區域(dll模組的全域性常量區,不過對映的資料區域為備份),隨著模組的解除安裝,該對映區域也會消失,導致訪問異常。
32     //至於為什麼顯示載入dll的方式不能呼叫非虛擬函式,是因為呼叫這種函式不需要查虛表,直接調函式地址,但該函式匯出名字經過修飾,
33     //會造成無法解析的引用; 子類和父類都有一套虛表,存的是各自的函式地址。
34     int* vb = d->virtualFunc();//ecx暫存器儲存的是this指標,即d;
35     d2->a = 2;
36     _asm
37     {
38         mov ecx, dword ptr[d2];
39     }
40     nor();//此時呼叫的是d2的成員函式。
41     delete d;
42     int *local = new int[10];
43     vb[0] = 1;
44     local[0] = 2;
45     int c = vb[0] + local[0];
46 
47     ::FreeLibrary(hmodule);
48     //int* va = d->virtualFunc();//報錯
49     return 0;
50 }

 顯示載入後,得到類物件d,是不能直接通過該物件呼叫其非虛成員函式的(連結不通過),但是能直接呼叫虛擬函式。問題是因為呼叫虛擬函式是要查虛表的。下圖是project.obj的main部分反彙編程式碼:

 

 可以看到對於一般的函式呼叫會生成函式符號,相當於一個佔位標記,該符號地址在連結前,用預設地址00 00 00 00 代替(32位機器下),在執行連結後,該預設地址會修改為正確的位置。

連結後的main部分反編譯程式碼:

 

 回到之前的那個問題,為什麼一般的成員函式不能直接呼叫,因為找不到符號(無法解析的引用符號),會導致連結不過。

 第一,匯出該符號(整個類都是匯出的話,該成員函式自然也是匯出的)。第二,該符號的名字要寫對;

NormalFunc nor = (NormalFunc)GetProcAddress(hmodule, "?normalFunc@Derive@@QAEPAHXZ");

強行獲取該方法。那麼又有一個問題,這個函式該怎麼呼叫?對於任意一個成員函式來講,呼叫會存在一個this指標。直接呼叫會出現奇怪的現象。其實通常呼叫成員函式,從彙編的角度,會將this指標賦值給ecx暫存器。接著呼叫該函式。

 

 

 

 上圖可以看到ecx與this的關係。通過證實nor()執行的確實是d2的成員函式。

接著下一個問題,解除安裝模組後,在該模組申請的堆記憶體資料還在不在?以及能不能繼續呼叫該模組的成員函式。

下圖先給出該程式的記憶體佈局(x64Dbg反編譯工具):

 

 執行完LoadLibrary後的記憶體佈局:

 

 可以看到dll_1對映到了某個記憶體地址。

 檢視dll中normalFunc的函式地址:

 對應於dll的程式碼段對映區域。

檢視d和d2的記憶體區域:

 

 可以看到這兩個變數所對應的首4位元組值是一樣的,這就是虛表地址。

轉到虛表地址:

 

 發現該虛表儲存在DLL_1的記憶體區域“.rdata ”段(從前面的記憶體佈局看出)。

那麼當真個DLL被解除安裝時發生了什麼?執行完Freelibrary後:

 

 

 

 

 

 那麼顯而易見,解除安裝dll模組後,變數d2是不能呼叫任何函式的,因為此時地址都清空了,包括虛擬函式,虛表不存在。而d2這個變數所對應的記憶體空間依然存在。但是意味著該類物件沒法呼叫解構函式,造成記憶體洩漏。

其實,在dll申請的記憶體,最好在該dll裡釋放,不然會出現奇怪的現象。

。。。待續

 

相關文章