第19章 DLL基礎
Windows API提供的所有函數都包含在DLL中。三個最重要的DLL:Kernel32.dll(管理記憶體、進程和線程)、Use32.dll(執行與使用者介面相關的任務)、GDI32.dll(繪製映像和顯示文字)。
19.1、DLL和進程的地址空間
第一層:DLL定位概要
在應用程式(或其他DLL)能調用一個DLL中函數前,必須將該DLL的檔案映像映射到調用進程的地址空間中。兩種方式:隱式載入時連結和顯示運行時連結。
映射後,當調用DLL中一個函數時,該函數會線上程棧中取得傳給他的參數,並用線程棧來存放它需要的局部變數。此外,DLL中函數建立的任何對象都為調用線程或進程所擁有。
19.2、縱觀全域
第二層:理論流程概要
若一個EXE需要從另一個DLL模組中匯入函數或變數,則需:
先構建DLL:
1)先建立一個標頭檔,包含在DLL中匯出的函數原型、結構及符號。為構建該DLL,DLL的所有源檔案需包含這個標頭檔。構建EXE時需同一個標頭檔。
2)建立源檔案來實現DLL模組中匯出的函數和變數。
3)構建DLL模組時,編譯器會對每個源檔案處理並產生一個.obj模組。
4)當所有.obj模組都建立完後,連結器將所有.obj模組內容合并,產生一個單獨的DLL。
5)若連結器檢測到DLL源檔案輸出了至少一個函數或變數,則連結器還會產生一個.lib檔案。它只是列出所有被匯出的函數或變數的符號名。
再構建EXE(可執行模組):
1)所有引用了匯出的函數、變數、資料結構或符號的源檔案中,必須包含DLL對應的標頭檔。
2)構建EXE時,編譯器會對每個源檔案處理並產生一個.obj模組。
3)編譯完後,連結器會將所有.obj模組內容合并產生一個exe。該exe(可執行模組)包含一個匯入段,其中列出了所有它需要的DLL模組,以及它從每個DLL模組中引用的符號。執行exe,OS的載入程式會執行5)。
4)載入程式先為新的進程建立一個虛擬位址空間,並將可執行模組映射到新進程的地址空間中。載入程式接著解析可執行模組的匯入段。對匯入段中列出的每個DLL,載入程式會在使用者系統中對該DLL模組進行定位,並將該DLL映射到進程的地址空間中。由於DLL模組可從其他DLL模組中匯入函數和變數,因此DLL模組可能有自己的匯入段並需將它所有的DLL模組映射到進程的地址空間中。
載入程式將EXE和所有DLL映射到進程的地址空間後,進程的主線程可以開始執行。
第三層:實踐詳細流程
19.2.1、構建DLL模組
一個DLL可匯出變數、函數或C++類。應避免匯出變數。僅當匯出的C++類的模組使用的編譯器與匯入的C++類的模組使用的編譯器由同一廠商提供時,才可匯出C++類。
巧妙之處(代碼P515):DLL標頭檔中
#ifdef MYLIBAPI
#else
#define MYLIBAPI extern “C” _declspec(dllimport)
#endif
//匯出的函數或變數
MYLIBAPI int Add(int nLeft, int nRight);
DLL源檔案中
#define MYLIBAPI extern “C” _declspec(dllexport) //必須在dll標頭檔之前
#include “dll標頭檔”
EXE源檔案中
#include “dll標頭檔” //不能定義MYLIBAPI宏
如此在DLL源檔案中MYLIBAPI被定義為匯出,EXE源檔案中MYLIBAPI被定義為匯入。
_declspec(dllexport):源檔案中不必在被匯出變數和函數前加此修飾。因編譯器在解析標頭檔時會記住應匯出哪些變數和函數。
_declspec(dllimport):DLL的標頭檔中加在匯出的函數、變數或C++類前。非必需,但能略微提高效率。EXE的源檔案中,編譯器看到此符號,會知道該從DLL模組中匯入該符號。
extern “C”:在編寫C++代碼時才使用(C不該使用)。因C++編譯器通常會對函數名和變數名進行改編,連結時會出錯。此修飾符是告訴編譯器不要對變數名或函數進行改編。
_stdcall:即使是C,當用此約定時,Microsoft的編譯器會對函數名進行改編。具體方法是:給函數名添加底線首碼和一個特殊的尾碼。該尾碼由一個@符號跟作為參數傳給函數的位元組數組成。如:_declspec(dllexport)
LONG _stdcall MyFunc(int a, int b);匯出為_MyFunc@8。所以要防止改編。兩種方式:建立一個.def檔案,並在.def檔案中包含一下類似下面段:
EXPORTS
MyFunc
第二種方法(建議不用)是匯出未經改編的函數名。在DLL的源檔案中加入:
#pragma comment(linker, “/export:MyFunc=_MyFunc@8”)
.def檔案格式:
LIBRARY XX(dll名稱這個並不是必須的,但必須確保跟產生的dll名稱一樣)
EXPORTS
[函數名] @ [函數序號]
匯出類:注意匯出類和使用匯出類同匯出函數和使用匯出函數類似,但在匯出類中不可使用extern “C”符號。
匯出段:當Microsoft C/C++編譯器看到__declspec(dllexport)修飾的變數、函數或C++類時,會在產生的.obj檔案中嵌入一些額外的資訊。當連結器在連結DLL的所有.obj檔案時,會解析這些資訊。連結器會在產生的DLL檔案中嵌入一個匯出符號表。這個匯出段列出了匯出的變數、函數或類的符號名。還會儲存相對虛擬位址,表示每個符號可在DLL模組的何處找到。(DumpBin.exe工具加入-exports可查看一個DLL的匯出段)
匯入段:當連結器看到__declspec(dllimport)修飾的匯入符號時,會在產生的可執行模組中嵌入一個特殊的段,它的名字叫匯入段。匯入段列出了該模組所需的DLL模組,以及它從每個DLL模組中引用的符號。(DumpBin.exe工具加入-imports可查看一個DLL的匯出段)
/***********************************Moudle: MyLib.h***********************************/#ifdef MYLIBAPI//MYLIBAPI should be defined in all of the DLL's source code //moudles before this header file is included.//All functions/variables are being exported.#else//This header file is included by an EXE source code moudle///Indicate that all functions/variables are being imported.#define MYLIBAPI extern "C" _declspec(dllimport)#endif////////////////////////////////////////////////Define any data structures and symbols here.////////////////////////////////////////////////Define exported variables here.(Note:Avoid exporting variables.)MYLIBAPI int g_nResult;///////////////////////////////////////////////Define exported function prototypes here.MYLIBAPI int Add(int nLeft, int nRight);////////////////////////End of File////////////////////////****************************************Moudle:MyLibFile1.cpp****************************************/#include <windows.h>#define MYLIBAPI extern "C" _declspec(dllexport)#include "MyLib.h"/////////////////////////////////////////int g_nResult;int Add(int nLeft, int nRight){g_nResult = nLeft + nRight;return g_nResult;}///////////////////End of File/////////////
19.2.2、構建可執行模組
1)包含dll的匯出標頭檔:#include <>;注意不要定義MYLIBAPI宏。
2)包含lib檔案:#pragma comment(lib, “”);為了讓連結器確定代碼中的匯入符號來自哪個DLL。
3)直接使用匯出的變數、函數或C++類。
19.2.3、運行可執行模組
OS的載入程式先為進程建立虛擬位址空間,然後將EXE映射到地址空間中,之後載入程式回檢查EXE的匯入段,對所需DLL進行定位並將它們映射到進程的地址空間中。
因匯入段只包含DLL名稱,不包含DLL路徑,因此載入程式必須搜尋DLL,順序為:
1)
包含可執行檔的目錄
2)
Windows的系統目錄,可通過GetSystemDirectory得到
3)
16位的系統目錄,即Windows目錄的System子目錄
4)
Windows目錄,可通過GetWindowsDirectory得到
5)
進程的目前的目錄
6)
PATH環境變數中列出的目錄。
比如現在建立好了一個DLL匯出了CMyClass類,客戶也能正常使用這個DLL,假設CMyClass對象的大小為30位元組。如果我們需要修改DLL 中的CMyClass類,讓它有相同的函數和成員變數,但是給增加了一個私人的成員變數int類型,現在CMyClass對象的大小就是34位元組了。當直接把這 個新的DLL給客戶使用替換掉原來30位元組大小的DLL,客戶應用程式期望的是30位元組大小的對象,而現在卻變成了一個34位元組大小的對象,糟糕,客戶程式出錯 了。
類似的問題,如果不是匯出CMyClass類,而在匯出的函數中使用了CMyClass,改變對象的大小仍然會有問題的。這個時候修改這個問題的唯一辦法就是替 換客戶程式中的CMyClass的標頭檔,全部重新編譯整個應用程式,讓客戶程式使用大小為34位元組的對象。
這就是一個嚴重的問題,有的時候如果沒有客戶程式的原始碼,那麼我們就不能使用這個新的DLL了。
具體用到時再baidu