問題起因是,在一次模塊卸載后,,程序運(yùn)行異常,。遂對動態(tài)鏈接庫做一些測試,。 動態(tài)庫加載方式有兩種,,隱式加載和顯示加載,隱式加載包含xxx.lib導(dǎo)入庫,,在程序執(zhí)行之前由動態(tài)加載器完成所有加載,;顯示加載則使用LoadLibrary方式;具體數(shù)據(jù)可參考《程序員的自我修養(yǎng):鏈接,,裝載與庫》一書,。 動態(tài)庫頭文件: 1 #ifdef DYNAMICLIBRARYTEST_EXPORTS 2 #define DYNAMICLIBRARYTEST_API __declspec(dllexport) 3 #else 4 #define DYNAMICLIBRARYTEST_API __declspec(dllimport) 5 #endif 6 7 // 此類是從 dll 導(dǎo)出的 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(); 動態(tài)庫實(shí)現(xiàn)文件: 1 // DynamicLibraryTest.cpp : 定義 DLL 的導(dǎo)出函數(shù),。 2 // 3 4 #include "DynamicLibraryTest.h" 5 // 這是導(dǎo)出變量的一個示例 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 // 這是導(dǎo)出函數(shù)的一個示例。 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 } 查看導(dǎo)出符號:
可以看到導(dǎo)出的變量命名比較正常,,這是因?yàn)槭且訡風(fēng)格導(dǎo)出的,。不然就是C++的詭異風(fēng)格修飾。 主程序?qū)崿F(xiàn):project.cpp 1 // project.cpp : 此文件包含 "main" 函數(shù),。程序執(zhí)行將在此處開始并結(jié)束,。 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();//不能直接調(diào)用非虛函數(shù) 30 //本模塊保存了一份虛表地址在堆上,,每次訪問虛函數(shù),通過堆上的保存的虛表地址查找真正的虛表,, 31 //而虛表保存在映射區(qū)域(dll模塊的全局常量區(qū),不過映射的數(shù)據(jù)區(qū)域?yàn)閭浞荩?,隨著模塊的卸載,,該映射區(qū)域也會消失,導(dǎo)致訪問異常,。 32 //至于為什么顯示加載dll的方式不能調(diào)用非虛函數(shù),,是因?yàn)檎{(diào)用這種函數(shù)不需要查虛表,直接調(diào)函數(shù)地址,,但該函數(shù)導(dǎo)出名字經(jīng)過修飾,, 33 //會造成無法解析的引用; 子類和父類都有一套虛表,存的是各自的函數(shù)地址,。 34 int* vb = d->virtualFunc();//ecx寄存器保存的是this指針,,即d; 35 d2->a = 2; 36 _asm 37 { 38 mov ecx, dword ptr[d2]; 39 } 40 nor();//此時調(diào)用的是d2的成員函數(shù),。 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,是不能直接通過該對象調(diào)用其非虛成員函數(shù)的(鏈接不通過),,但是能直接調(diào)用虛函數(shù),。問題是因?yàn)檎{(diào)用虛函數(shù)是要查虛表的。下圖是project.obj的main部分反匯編代碼:
可以看到對于一般的函數(shù)調(diào)用會生成函數(shù)符號,,相當(dāng)于一個占位標(biāo)記,,該符號地址在鏈接前,用默認(rèn)地址00 00 00 00 代替(32位機(jī)器下),,在執(zhí)行鏈接后,,該默認(rèn)地址會修改為正確的位置。 鏈接后的main部分反編譯代碼:
回到之前的那個問題,,為什么一般的成員函數(shù)不能直接調(diào)用,,因?yàn)檎也坏椒枺o法解析的引用符號),會導(dǎo)致鏈接不過,。 第一,,導(dǎo)出該符號(整個類都是導(dǎo)出的話,該成員函數(shù)自然也是導(dǎo)出的),。第二,,該符號的名字要寫對; NormalFunc nor = (NormalFunc)GetProcAddress(hmodule, "?normalFunc@Derive@@QAEPAHXZ"); 強(qiáng)行獲取該方法,。那么又有一個問題,,這個函數(shù)該怎么調(diào)用,?對于任意一個成員函數(shù)來講,調(diào)用會存在一個this指針,。直接調(diào)用會出現(xiàn)奇怪的現(xiàn)象,。其實(shí)通常調(diào)用成員函數(shù),從匯編的角度,,會將this指針賦值給ecx寄存器,。接著調(diào)用該函數(shù)。
上圖可以看到ecx與this的關(guān)系,。通過證實(shí)nor()執(zhí)行的確實(shí)是d2的成員函數(shù),。 接著下一個問題,卸載模塊后,,在該模塊申請的堆內(nèi)存數(shù)據(jù)還在不在,?以及能不能繼續(xù)調(diào)用該模塊的成員函數(shù)。 下圖先給出該進(jìn)程的內(nèi)存布局(x64Dbg反編譯工具):
執(zhí)行完LoadLibrary后的內(nèi)存布局:
可以看到dll_1映射到了某個內(nèi)存地址,。 查看dll中normalFunc的函數(shù)地址: 對應(yīng)于dll的代碼段映射區(qū)域,。 查看d和d2的內(nèi)存區(qū)域:
可以看到這兩個變量所對應(yīng)的首4字節(jié)值是一樣的,這就是虛表地址,。 轉(zhuǎn)到虛表地址:
發(fā)現(xiàn)該虛表存儲在DLL_1的內(nèi)存區(qū)域“.rdata ”段(從前面的內(nèi)存布局看出),。 那么當(dāng)真?zhèn)€DLL被卸載時發(fā)生了什么?執(zhí)行完Freelibrary后:
那么顯而易見,,卸載dll模塊后,,變量d2是不能調(diào)用任何函數(shù)的,因?yàn)榇藭r地址都清空了,,包括虛函數(shù),,虛表不存在。而d2這個變量所對應(yīng)的內(nèi)存空間依然存在,。但是意味著該類對象沒法調(diào)用析構(gòu)函數(shù),,造成內(nèi)存泄漏。 其實(shí),,在dll申請的內(nèi)存,,最好在該dll里釋放,不然會出現(xiàn)奇怪的現(xiàn)象,。 ,。。,。待續(xù)
|
|