介紹
動態連結提供了一種方法,使進程可以調用不屬於其
可執行代碼的函式。函式的可執行代碼位於一個 DLL 檔案中,該 DLL 包含一個或多個已被編譯、連結並與使用它們的進程分開存儲的函式。DLL 還有助於共享數據和資源。多個應用程式可同時訪問記憶體中單個 DLL 副本的內容。
使用動態程式庫可以更為容易地將更新套用於各個
模組,而不會影響該程式的其他部分。例如,您有一個大型網路遊戲,如果把整個數百MB甚至數GB的遊戲的代碼都放在一個應用程式里,日後的修改工作將會十分費時,而如果把不同功能的代碼分別放在數個
動態連結庫中,您無需重新生成或安裝整個程式就可以套用更新。
動態連結庫檔案,是一種不可執行的
二進制程式檔案,它允許程式共享執行特殊任務所必需的代碼和其他資源。Windows 提供的DLL檔案中包含了允許基於 Windows 的程式在 Windows 環境下操作的許多函式和資源。一般被存放在電腦的"C:\Windows\System32" 目錄下。
Windows 中,DLL 多數情況下是帶有 ".dll" 擴展名的檔案,但也可能是 ".ocx"或其他擴展名;
Linux系統中常常是 ".so" 的檔案。它們向運行於 Windows作業系統下的程式提供代碼、數據或函式。程式可根據 DLL 檔案中的指令打開、啟用、查詢、禁用和關閉
驅動程式。
背景
DLL的最初目的是節約應用程式所需的磁碟和記憶體空間。在一個傳統的非共享庫中,一部分代碼簡單地附加到調用的程式上。如果兩個程式調用同一個子程式,就會出現兩份那段代碼。相反,許多套用共享的代碼能夠切分到一個DLL中,在硬碟上存為一個檔案,在記憶體中使用一個實例(instance)。DLL的廣泛套用使得早期的視窗能夠在緊張的記憶體條件下運行。
DLL提供了如模組化這樣的共享庫的普通好處。模組化允許僅僅更改幾個應用程式共享使用的一個DLL中的代碼和數據而不需要更改應用程式自身。這種模組化的基本形式允許如Microsoft Office、Microsoft Visual Studio、甚至Microsoft Windows自身這樣大的應用程式使用較為緊湊的補丁和服務包。
模組化的另外一個好處是外掛程式的通用接口使用。單個的接口允許舊的模組與新的模組一樣能夠與以前的應用程式運行時無縫地集成到一起,而不需要對應用程式本身作任何更改。這種動態擴展的思想在ActiveX中發揮到了極致。
儘管有這么多的優點,使用DLL也有一個缺點:DLL地獄,也就是幾個應用程式在使用同一個共享DLL庫發生版本衝突。這樣的衝突可以通過將不同版本的問題DLL放到應用程式所在的資料夾而不是放到系統資料夾來解決;但是,這樣將抵消共享DLL節約的空間。目前,Microsoft .NET將解決DLL hell問題當作自己的目標,它允許同一個共享庫的不同版本並列共存。由於現代的計算機有足夠的磁碟空間和記憶體,這也可以作為一個合理的實現方法。
特徵
記憶體管理
在
Win32中,DLL檔案按照片段(sections)進行組織。每個片段有它自己的屬性,如可寫或是唯讀、可執行(代碼)或者不可執行(數據)等等。這些section可分為兩種,一個是與絕對地址定址無關的,所以能被多進程公用;另一個是與絕對地址定址有關的,這個就必須由每個進程有自己的副本專用。sections的這種二分類,在編譯DLL時就已經由編譯器、連結器給標註好了。所以在裝入DLL時,裝入器知道哪些sections在記憶體物理地址空間只需要有一份,供多個進程共用(映射到各個進程的記憶體邏輯地址空間,所以邏輯地址可以不同); 哪些sections必須是進程使用自己的專用副本。
具體說,DLL裝入時需考慮下述情形:
局部變數——每個執行緒都有自己的棧,DLL內部的局部變數隨所在函式被執行而在各自執行緒的調用棧上開闢存儲空間。
全局變數
const全局變數——放入const節中,多進程共享;
非const全局變數——放入各個進程各自專用的data節中。即DLL裝入時各個進程複製一份自己專用的DLL的data節。但是,對於一個進程內的多個執行緒並發訪問這種進程空間全局變數,仍然存線上程安全問題。例如,在一個COM的DLL載入入一個進程的空間後,該進程的多個執行緒可能會並發訪問該COM庫的COM對象。為此,Windows與COM引入了執行緒“套間”(apartment)技術。一個進程內,應用程式與載入的各個DLL分屬於不同的Module,如果DLL使用所在Module的全局變數,例如動態連結MFC的regular dll在訪問自己的MFC全局變數時,應該明確聲明。
DLL內部定義的全局變數
訪問DLL以外定義的全局變數——使用間址技術,在DLL的data節中用一個指針數據類型的記憶體空間來保存一個外部全局變數的地址。
函式調用
調用DLL內部定義的函式。這不是問題。
調用DLL外部定義的函式。例如,DLL內部調用一個外部函式foo()。這個foo函式在進程1中可能實現為“四捨五入”,在進程2中實現為“下取整”。所以調用外部函式是各個進程私用的事情。解決辦法是使用間址技術,在data節中用一個“函式指針”數據類型的記憶體空間來保存這種外部函式的入口地址。
跳轉指令
DLL內部跳轉,不是問題
跳轉到DLL外部,解決同上述3.2
DLL代碼段通常被使用這個DLL的所有進程所共享。如果代碼段所占據的物理記憶體被收回,它的內容就會被放棄,後面如果需要的話就直接從DLL檔案重新載入。
與代碼段不同,DLL的數據段通常是私有的;也就是說,每個使用DLL的進程都有自己的DLL數據副本。作為選擇,數據段可以設定為共享,允許通過這個共享記憶體區域進行進程間通信。但是,因為用戶許可權不能套用到這個共享DLL記憶體,這將產生一個安全漏洞;也就是一個進程能夠破壞共享數據,這將導致其它的共享進程異常。例如,一個使用訪客賬號的進程將可能通過這種方式破壞其它運行在特權賬號的進程。這是在DLL中避免使用共享片段的一個重要原因。
當DLL被如UPX這樣一個可執行的packer壓縮時,它的所有代碼段都標記為可以讀寫並且是非共享的。可以讀寫的代碼段,類似於私有數據段,是每個進程私有的並且被頁面檔案備份。這樣,壓縮DLL將同時增加記憶體和磁碟空間消耗,所以共享DLL應當避免使用壓縮DLL。
符號解析和綁定
DLL輸出的每個函式都由一個數字序號唯一標識,也可以由可選的名字標識。同樣,DLL引入的函式也可以由序號或者名字標識。對於內部函式來說,只輸出序號的情形很常見。對於大多數視窗API函式來說名字是不同視窗版本之間保留不變的;序號有可能會發生變化。這樣,我們不能根據序號引用視窗API函式。
按照序號引用函式並不一定比按照名字引用函式性能更好:DLL輸出表是按照名字排列的,所以對半查找可以用來在在這個表中根據名字查找這個函式。另外一方面,只有線性查找才可以用於根據序號查找函式。
將一個執行檔綁定到一個特定版本的DLL也是可能的,這也就是說,可以在編譯時解析輸入函式(imported functions)的地址。對於綁定的輸入函式,連結工具保存了輸入函式綁定的DLL的時間戳和校驗和。在運行時Windows檢查是否正在使用同樣版本的庫,如果是的話,Windows將繞過處理輸入函式;否則如果庫與綁定的庫不同,Windows將按照正常的方式處理輸入函式。
綁定的執行檔如果運行在與它們編譯所用的環境一樣,函式調用將會較快,如果是在一個不同的環境它們就等同於正常的調用,所以綁定輸入函式沒有任何的缺點。例如,所有的標準Windows應用程式都綁定到它們各自的Windows發布版本的系統DLL。將一個應用程式輸入函式綁定到它的目的環境的好機會是在應用程式安裝的過程。
運行時顯式連結
對每個DLL來說,Windows存儲了一個全域計數器,每多一個進程使用便多額外一個。LoadLibrary與FreeLibrary指令影響每一個進程內含的計數器;動態連結則不影響。因此藉由調用FreeLibrary多次,從存儲器卸載一DLL是很重要的。一個進程可以從它自己的VAS註銷此計數器。
DLL檔案能夠在運行時使用LoadLibrary(或者LoadLibraryEx)API函式進行顯式調用,這個的過程微軟簡單地稱為運行時動態調用。API函式GetProcAddress根據查找輸出名稱符號、FreeLibrary卸載DLL。這些函式類似於POSIX標準API中的dlopen、dlsym、和dlclose。
注意微軟簡單稱為運行時動態連結的運行時隱式連結,如果不能找到連結的DLL檔案,Windows將提示一個錯誤訊息並且調用應用程式失敗。應用程式開發人員不能通過編譯連結來處理這種缺少DLL檔案的隱式連結問題。另外一方面,對於顯式連結,開發人員有機會提供一個完善的出錯處理機制。
優點
擴展了應用程式的特性;
可以用許多種程式語言來編寫;
簡化了軟體項目的管理;
有助於節省記憶體;
有助於資源共享;
有助於應用程式的本地化;
有助於解決平台差異;
可以用於一些特殊的目的。Windows 使得某些特性只能為 DLL 所用。
依賴項
當某個程式或 DLL 使用其他 DLL 中的 DLL 函式時,就會創建依賴項。因此,該程式就不再是獨立的,並且如果該依賴項被損壞,該程式就可能遇到問題。例如,如果發生下列操作之一,則該程式可能無法運行:
依賴 DLL 升級到新版本。
修復了依賴 DLL。
依賴 DLL 被其早期版本覆蓋。
從計算機中刪除了依賴 DLL。
這些操作通常稱為 DLL 衝突。如果沒有強制實現向後兼容性,則該程式可能無法成功運行。
入口點
在創建 DLL 時,可以有選擇地指定入口點函式。當進程或
執行緒將它們自身附加到 DLL 或者將它們自身從 DLL 分離時,將調用入口點函式。您可以使用入口點函式根據 DLL 的需要來初始化數據結構或者銷毀數據結構。此外,如果應用程式是多執行緒的,則可以在入口點函式中使用執行緒本地存儲(TLS) 來分配各個執行緒專用的記憶體。下面的代碼是一個 DLL 入口點函式的示例:
BOOL APIENTRY DllMain( HANDLE hModule, // DLL模組的句柄 DWORD ul_reason_for_call, // 調用本函式的原因 LPVOID lpReserved // 保留) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //進程正在載入本DLL break; case DLL_THREAD_ATTACH: //一個執行緒被創建 break; case DLL_THREAD_DETACH: //一個執行緒正常退出 break; case DLL_PROCESS_DETACH: //進程正在卸載本DLL break; } return TRUE; //返回TRUE,表示成功執行本函式}
當
入口點函式返回 FALSE 值時,如果您使用的是載入時
動態連結,則應用程式不啟動。如果您使用的是運行時動態連結,則只有個別 DLL 不會載入。
注意:在多執行緒應用程式中,請確保將對 DLL 全局數據的訪問進行同步(
執行緒安全),以避免可能的數據損壞。為此,請使用 TLS 為各個
執行緒提供唯一的數據。
如何導出
要導出 DLL 函式,您可以嚮導出的 DLL 函式中添加函式關鍵字,也可以創建
模組定義檔案(.def) 以列出導出的 DLL 函式。
(1)嚮導出的 DLL 函式中添加函式關鍵字
要使用函式
關鍵字,您必須使用以下關鍵字來聲明要導出的各個函式:
要在應用程式中使用導出的 DLL 函式,您必須使用以下關鍵字來聲明要導入的各個函式:
__declspec(dllimport)
通常情況下,您最好使用一個包含 define 語句和 ifdef 語句的頭檔案,以便分隔導出語句和導入語句。
(2)創建模組定義檔案以列出導出的 DLL 函式
使用
模組定義檔案來聲明導出的 DLL 函式。當您使用模組定義檔案(.def)時,您不必嚮導出的 DLL 函式中添加函式關鍵字。在模組定義檔案中,您可以聲明 DLL 的 LIBRARY 語句和 EXPORTS 語句。
特別調用
關於特定情況下的調用,比如DLL函式中使用到了 Win32 API 或者將 C++ 生成的 DLL 供標準C語言使用,則需要注意以下一些情況:
如果使用到了 Win32 API,則應該使用關鍵字 __stdcall
在將 C++ 生成的 DLL 供標準C語言使用時,輸出檔案需要用 extern "C" 修飾,否則不能被標準
C語言調用。如果使用 __stdcall 調用方式,可能產生C不識別的修飾名,所以設定導出函式時要採用 .def 檔案形式,而不是__declspec(dllexport) 形式。後者會進行修飾名轉換,C語言無法識別函式。
// SampleDLL.def//LIBRARY "SampleDLL"EXPORTS
示例
HelloWorld 示例 DLL 和應用程式
// SampleDLL.cpp#include "stdafx.h"#define EXPORTING_DLL#include "SampleDLL.h"BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE;}void HelloWorld() { MessageBox( NULL, TEXT("Hello World"), TEXT("In a DLL"), MB_OK);}// File: SampleDLL.h#ifndef INDLL_H #define INDLL_H #ifdef EXPORTING_DLLextern __declspec(dllexport) void HelloWorld() ; #elseextern __declspec(dllimport) void HelloWorld() ; #endif#endif
下面的代碼是一個“Win32應用程式”項目的示例,該示例調用 SampleDLL DLL 中的導出 DLL 函式。
// SampleApp.cpp#include "stdafx.h"#include "SampleDLL.h"int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow) { HelloWorld(); return 0;}
注意:在載入時
動態連結中,您必須連結在生成 SampleDLL 項目時創建的 SampleDLL.lib
導入庫。
在運行時動態連結中,您應使用與以下代碼類似的代碼來調用 SampleDLL.dll導出 DLL 函式。
//...typedef VOID (*DLLPROC) (LPTSTR);//...HINSTANCE hinstDLL;DLLPROC HelloWorld;BOOL fFreeDLL;hinstDLL = LoadLibrary("sampleDLL.dll");if (hinstDLL != NULL){ HelloWorld = (DLLPROC) GetProcAddress(hinstDLL, "HelloWorld"); if (HelloWorld != NULL) (HelloWorld); fFreeDLL = FreeLibrary(hinstDLL);}//...
DLL描述
kernel32.dll
user32.dll
與 Windows 管理有關的函式。訊息、選單、
游標、
計時器、通信和其他大多數非現實函式都可以從這裡找到。
gdi32.dll
圖形設備接口庫。與設備輸出有關的函式:大多數繪圖、顯示場景、
圖元檔案、坐標及其字型函式都可以從這裡找到。
comdlg32.dll / lz32.dll / version.dll
comctl32.dll
一個新的 Windows 控制項集合,比如 TreeView 和 RichTextBox 等等,最初這個是為了 Windows 95 而製作的,但是也使用於 NT 下。
mapi32.dll
電子郵件的專用函式。
netapi32.dll
訪問和控制網路的函式。
odbc32.dll