問題起因是,在一次模塊卸載后,程式運行例外,遂對元件做一些測驗,
動態庫加載方式有兩種,隱式加載和顯示加載,隱式加載包含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里釋放,不然會出現奇怪的現象,
,,,待續
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/257005.html
標籤:C++
