來源: Mars
介紹一點重要的背景知識:
所有的Win32API函數都包含在DLL中。三個最重要的DLL是:KERNEL32.DLL(它由管理內存、進程和線程的函數組成),USER32.DLL(它由執行用戶界面任務(如創建窗口和發送消息)的函數組成),GDI32.DLL(它由繪圖和顯示文本的函數組成)。另外還有一些執行專門功能的DLL,例如:ADVAPI32.DLL(包含有關對象安全、註冊表管理和事件記錄的函數);COMDLG32.DLL(包含了通用對話框,FileOpen、FileSave等);LZ32.DLL(包含文件解壓縮函數)
13.1 創建動態鏈接庫
DLL中通常沒有處理消息循環的代碼和創建窗口的代碼。
應用程序要呼叫DLL中的函數,必須先把DLL的文件映射到進程的地址空間裡。
由 DLL中代碼創建的對象都歸呼叫線程或進程所有,DLL在Win32中什麼也不擁有。例如:DLL中的函數呼叫了VirtualAlloc,保留的地址空 間區域在呼叫的線程的進程的地址空間中。如果後來DLL從程序中釋放了,可是由DLL保留的這塊物理存儲並不釋放,只有當程序某個線程呼叫了 VirtualFree或是進程終結時,該保留區域才從內存中釋放。
13.1.1將DLL映射到進程的地址空間:可以使用兩種方法將DLL映射到進程的地址空間,一種是隱式連接,另一種是顯式裝入。
1.隱式鏈接:
採用這種方法時,系統尋找DLL文件時,查找以下這幾處:
(1)包含EXE文件的目錄
(2)進程的當前目錄
(3)Windows系統目錄
(4)Windows目錄
(5)列在PATH環境變量中的目錄
如果這五處都找不到DLL,那麼系統會顯示一個消息框,之後終止整個進程。
採用這種方法映射的DLL,DLL在進程終結時才會被解除映射,也就是說在進程終結時才會被釋放。
2.顯式鏈接:
當進程中的線程呼叫LoadLibrary或LoadLibraryEx時可以顯示的映射DLL文件到進程的地址空間。顯式加載DLL後,可以在任意時刻用 FreeLibrary來釋放DLL在進程地址空間中的映射。如果同一進程中的線程使用LoadLibrary或LoadLibraryEx加載了相同的 DLL兩次以上,那麼在第一次加載DLL到進程的地址空間後,系統並不會再重複加載DLL,而只是增加對DLL的使用計數,同理在使用 FreeLibrary時,只是減少對DLL的使用計數,並一定要從進程的地址空間中釋放DLL。當系統看到一個DLL的使用計數為0時,就自動把該 DLL從進程的地址空間中釋放。如果是兩個不同的進程(進程A和進程B)顯示載入了相同的DLL,那麼進程A在載入DLL時,系統同時為該DLL的使用計 數設置為1,同理B的也是1,在進程A釋放DLL時,進程B中的DLL並不受任何影響,進程B中對DLL的使用計數還是1,而進程A中對該DLL的使用計 數變成0。註:顯示鏈接時,系統尋找DLL文件同隱式鏈接,不過函數LoadLibraryEx可以改變尋找的方式。
線程可以呼叫GetModuleHandle函數來判斷一個DLL是否被載入了進程的地址空間,
HINSTANCE GetModuleHandle(LPCTSTR lpszModuleName);
例子:
HINSTANCE hinstDLL;
hinstDLL = GetModuleHandle(「SomeDLL.dll」);
If (hinstDLL == NULL){
hinstDLL = LoadLibrary(「SomeDLL.dll」);
}
如果有了DLL的HINSTANCE值就可以使用GetModuleFileName來得到DLL的全路徑名,
DWORD GetModuleFileName(HINSTANCE hinstModule, LPTSTR lpszPath,DWORD cchPath);
參數說明:
hinstModule是DLL或EXE的HINSTANCE值,lpszPath是返回的DLL的全路徑名,cchPath指定緩衝區字符大小。
13.2 DLL的進入/退出函數
這些函數常常被DLL用來執行線程級或進程級的初始化或清理工作。
如果DLL中有這個進入/退出函數,那麼這個函數必須是下面這個形式的:
BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad){
Switch(fdwReason){
Case DLL_PROCESS_ATTACH:
//當這個DLL被映射到了進程的地址空間時
break;
case DLL_THREAD_ATTACH:
//一個線程正在被創建
break;
case DLL_THREAD_DETACH:
//線程終結
break;
case DLL_PROCESS_DETACH:
//這個DLL從進程的地址空間中解除映射
break;
}
return(TRUE);
}
參數說明:
hinstDLL包含DLL句柄。該值表示DLL被映射到進程地址空間內的虛擬地址。
fImpLoad當隱式加載時該參數非零,當DLL被顯示加載時為零。
fdwReason:該參數指出系統為什麼呼叫該函數。該參數有四個可能值:DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH。下面分別說明這四個可能值的作用:
DLL_PROCESS_ATTACH:
當一個DLL被首次載入進程地址空間時,系統會呼叫該DLL的DLLMain函數,傳遞的參數fdwReason為 DLL_PROCESS_ATTACH。這種情況只有在首次映射DLL時才發生。當DLLMain處理DLL_PROCESS_ATTACH 時,DLLMain函數的返回值表示DLL的初始化是否成功。成功返回TRUE,否則返回FALSE。舉一個在DLL_PROCESS_ATTACH通知 中簡單的初始化例子:使用HeapCreate來創建一個DLL要使用的堆,當然這個堆是在進程的地址空間上的。(現在描述一下隱式載入DLL時都發生了什麼: 當新建一個進程時,系統為該進程分配了地址空間,之後將EXE文件和所需要的DLL文件都映射到進程的地址空間。之後系統創建了進程的主線程,並使用這個 主線程來呼叫每一個DLL中的DLLMain函數,傳遞給DLLMain函數的參數fdwReason的值是DLL_PROCESS_ATTACH。在所 有的DLL都響應了DLL_PROCESS_ATTACH通知後,系統讓進程的主線程執行EXE的C運行時啟動代碼,而後呼叫EXE文件的WinMain 函數。如果有一個DLL的DLLMain函數的返回值為FALSE,說明DLL的初始化沒有成功,系統就會終結整個進程,去掉所有文件映像,之後顯示一個 對話框告訴用戶進程不能啟動。再說明一下顯示載入DLL時都發生了什麼:首先線程(A)呼叫LoadLibrary或 LoadLibraryEx來載入一個DLL,之後系統讓線程A來呼叫DLL中的DLLMain函數,並傳遞參數fdwReason值為 DLL_PROCESS_ATTACH,當DLL中的DLLMain處理完DLL_PROCESS_ATTACH通知後,線程就會從 LoadLibrary返回,繼續執行線程中LoadLibrary下面的代碼。如果DLL中的DLLMain返回FALSE,說明初始化不成功,系統將 DLL自動解除映射,使用對LoadLibrary或LoadLibraryEx的呼叫返回NULL。)
DLL_PROCESS_DETACH:
當DLL從進程的地址空間解除映射時,參數fdwReason被傳遞的值為DLL_PROCESS_DETACH。當DLL處理 DLL_PROCESS_DETACH時,DLL應該處理與進程相關的清理操作。舉個例子:可以在DLL_PROCESS_DETACH階段使用 HeapDestroy來釋放在DLL_PROCESS_DETACH階段創建的堆。
如 果進程的終結是因為系統中有某個線程呼叫了TerminateProcess來終結的,那麼系統就不會用DLL_PROCESS_DETACH來呼叫 DLL中的DLLMain函數來執行進程的清理工作。這樣就會造成數據丟失。所以萬不得以不要使用TerminateProcess來終結進程。
DLL_THREAD_ATTACH:
該通知告訴所有的DLL執行線程的初始化。當進程創建一個新線程時,系統會查看進程地址空間中所有的DLL文件映射,之後用 DLL_THREAD_ATTACH來呼叫DLL中的DLLMain函數。要注意:系統不會為進程的主線程使用值DLL_THREAD_ATTACH來呼 叫DLL中的DLLMain函數。
DLL_THREAD_DETACH:
該通知告訴所有的DLL執行線程的清理工作。注意如果線程的終結是使用TerminateThread來完成的,那麼系統將不會使用值 DLL_THREAD_DETACH來執行線程的清理工作,這也就是說可能會造成數據丟失,所以萬不得已不要使用TerminateThread來終結線 程。
註:在編寫DLL時不必一定實現DLLMain函數,當DLL源碼中沒有DLLMain函數時,C運行時庫有它自己的一個DLLMain函數。 13.3 從DLL中輸出函數和變量 在C中輸出函數和變量的方法: 舉例子說明:如何輸出函數add和變量g_nUsageCount, _declspec(dllexport) int Add(){ //函數Add的函數體 } _declspec(dllexport) int g_nUsageCount = 0; 下
面說明一下從DLL中輸出函數和變量時鏈接器所要做的一些事:首先在鏈接器鏈接DLL時,鏈接器檢查到了關於輸出函數和變量的信息,之後鏈接器就自動產生
了一個包含DLL輸出符號的LIB文件,同時還在生成的DLL文件中嵌入了一個輸出符號表(這個表中包含DLL中要輸出的函數和變量的名字和在DLL中對應的地址)。 13.4 在EXE中使用DLL輸出的函數和變量
下面舉例子說明如何在EXE中呼叫從DLL中輸出的函數Add和變量g_nUsageCount, _declspec(dllimport) int Add(); _declspec(dllimport) int g_nUsageCount; 線程可以使用GetProcAddress函數來得到從DLL中輸出的函數或變量的地址, FARPROC GetProcAddress(HINSTANCE hinstDLL,LPCSTR lpszProc); 參數說明:hinstDLL是DLL的句柄,lpszProc可以是DLL中函數或變量的名字,也可以是對應DLL中函數或變量的序號。 舉例子: GetProcAddress(hinst,」SomeDLLName」);//直接使用函數名 GetProcAddress(hinst,MakeIntResource(2));//使用函數的序號 13.5 在EXE或DLL的多個實例中共享數據 13.5.1 首先說明一下EXE或DLL中的節: 每一個EXE或DLL文件都是由節的集合組成的,每個節由「.」開始,例如:編譯器把所有代碼都放在.text節中。如下表所示為EXE或DLL中的常用節: 節名 內容 .text 應用程序或DLL的代碼 .bss 未初始化的數據 .rdata 只讀的運行時數據 .rsrc 資源 .edata 輸出名字表 .data 初始化的數據 .xdata 異常處理表 .idata 引入名字表 .CRT 只讀的C運行時數據 .reloc 修正表信息 .debug 調試信息 .tls 線程局部存儲 每個節都有如下訪問權限: 屬性 含義 READ 該節中的字節可讀取 WRITE 該節中的字節可寫入 EXECUTE 該節中的字節可執行 SHARED 該節中的字節可被多個實例共享 在代碼中使用節的方法舉例: #pragma comment(linker,」/SECTION:SHARED,RWS」);//使節SHARED可以讀、寫、可共享。
留言列表