動態連結庫

動態連結函式庫。所謂動態連結，就是把一些經常會共用的程式碼製作成DLL檔，當執行檔呼叫到DLL檔內的函式時，Windows作業系統才會把DLL檔載入記憶體內，DLL檔本身的結構就是可執行檔，當程式有需求時函式才進行連結。透過動態連結方式，記憶體浪費的情形將可大幅降低。靜態連結函式庫則是直接連結到執行檔。DLL的檔案格式與視窗EXE檔案一樣——也就是說，等同於32位視窗的可移植執行檔案。作為EXE格式，DLL可以包括原始碼、資料和資源的多種組合。在更廣泛的意義上說，任何同樣檔案格式的電腦檔案都可以稱作資源DLL。這樣的DLL的例子有副檔名為codice_4的圖示函式庫、副檔名為codice_5和codice_6的字型檔案。背景.DLL的最初目的是節約應用程式所需的磁碟和記憶體空間。在一個傳統的非共享函式庫中，一部分程式碼簡單地附加到呼叫的程式上。如果兩個程式呼叫同一個子程式，就會出現兩份那段程式碼。相反，許多應用共享的程式碼能夠切分到一個DLL中，在硬碟上存為一個檔案，在記憶體中使用一個例項。DLL的廣泛應用使得早期的視窗能夠在緊張的記憶體條件下執行。DLL提供瞭如模組化這樣的共享函式庫的普通好處。模組化允許僅僅更改幾個應用程式共享使用的一個DLL中的程式碼和資料而不需要更改應用程式自身。這種模組化的基本形式允許如Microsoft Office、Microsoft Visual Studio、甚至Microsoft Windows自身這樣大的應用程式使用較為緊湊的補丁和服務包。模組化的另外一個好處是外掛的通用介面使用。單個的介面允許舊的模組與新的模組一樣能夠與以前的應用程式執行時無縫地整合到一起，而不需要對應用程式本身作任何更改。這種動態擴充套件的思想在ActiveX中發揮到了極致。儘管有這麼多的優點，使用DLL也有一個缺點：DLL地獄，也就是幾個應用程式在使用同一個共享DLL函式庫發生版本衝突。這樣的衝突可以透過將不同版本的問題DLL放到應用程式所在的資料夾而不是放到系統資料夾來解決；但是，這樣將抵消共享DLL節約的空間。目前，Microsoft .NET將解決DLL hell問題當作自己的目標，它允許同一個共享函式庫的不同版本並列共存。由於現代的計算機有足夠的磁碟空間和記憶體，這也可以作為一個合理的實現方法。技術.記憶體管理.在Win32中，DLL檔案按照片段； 哪些sections必須是程序使用自己的專用副本。也可在程式編譯時透過編譯選項/section 的S (Shared)屬性，顯式指定哪個節是跨程序共享的。預設情況下，DLL的資料節都是寫時複製。具體說，DLL裝入時需考慮下述情形：DLL程式碼段通常被使用這個DLL的所有程序所共享。如果程式碼段所佔據的物理記憶體被收回，它的內容就會被放棄，後面如果需要的話就直接從DLL檔案重新載入。與程式碼段不同，DLL的資料段通常是私有的；也就是說，每個使用DLL的程序都有自己的DLL資料副本。作為選擇，資料段可以設定為共享，允許透過這個共享記憶體區域進行程序間通訊。但是，因為使用者許可權不能應用到這個共享DLL記憶體，這將產生一個安全漏洞；也就是一個程序能夠破壞共享資料，這將導致其它的共享程序異常。例如，一個使用訪客賬號的程序將可能透過這種方式破壞其它執行在特權賬號的程序。這是在DLL中避免使用共享片段的一個重要原因。當DLL被如UPX這樣一個可執行的packer壓縮時，它的所有程式碼段都標記為可以讀寫並且是非共享的。可以讀寫的程式碼段，類似於私有資料段，是每個程序私有的並且被頁面檔案備份。這樣，壓縮DLL將同時增加記憶體和磁碟空間消耗，所以共享DLL應當避免使用壓縮DLL。符號解析和繫結.DLL輸出的每個函式都由一個數字序號唯一標識，也可以由可選的名字標識。同樣，DLL引入的函式也可以由序號或者名字標識。對於內部函式來說，只輸出序號的情形很常見。對於大多數視窗API函式來說名字是不同視窗版本之間保留不變的；序號有可能會發生變化。這樣，我們不能根據序號引用視窗API函式。按照序號引用函式並不一定比按照名字引用函式效能更好：DLL輸出表是按照名字排列的，所以對半查詢可以用來在在這個表中根據名字查詢這個函式。另外一方面，只有線性查詢才可以用於根據序號查詢函式。將一個可執行檔案繫結到一個特定版本的DLL也是可能的，這也就是說，可以在編譯時解析輸入函式的地址。對於繫結的輸入函式，連結工具儲存了輸入函式繫結的DLL的時間戳和校驗和。在執行時Windows檢查是否正在使用同樣版本的函式庫，如果是的話，Windows將繞過處理輸入函式；否則如果函式庫與繫結的函式庫不同，Windows將按照正常的方式處理輸入函式。繫結的可執行檔案如果執行在與它們編譯所用的環境一樣，函式呼叫將會較快，如果是在一個不同的環境它們就等同於正常的呼叫，所以繫結輸入函式沒有任何的缺點。例如，所有的標準Windows應用程式都繫結到它們各自的Windows釋出版本的系統DLL。將一個應用程式輸入函式繫結到它的目的環境的好機會是在應用程式安裝的過程。執行時通知DLL程序/執行緒載入.程序/執行緒載入時，可以透過DllMain函式通知DLL相關資訊，提供對應處理的機會。BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad)switch(fdwReason)case DLL_PROCESS_ATTACH://當這個DLL第一次被對映到了這個程序的地址空間時。DLLMain函式的返回值為FALSE，說明DLL的初始化沒有成功，系統就會終結整個程序，去掉所有檔案映象，之後顯示一個對話方塊告訴使用者程序不能啟動。break;case DLL_THREAD_ATTACH://一個執行緒被建立，新建立的執行緒負責執行這次的DllMain函式。系統不會讓程序已經存在的執行緒以DLL_THREAD_ATTACH的值來呼叫DllMain函式。主執行緒永遠不會以DLL_THREAD_ATTACH的值來呼叫DllMain函式。系統是順序呼叫DllMain函式的，一個執行緒執行完DllMain函式才會讓另外一個執行緒執行DllMain函式。break;case DLL_THREAD_DETACH://如果執行緒呼叫了ExitThread來結束執行緒。執行緒呼叫了TerminateThread，系統就不會用值DLL_THREAD_DETACH來呼叫所有DLL的DllMain函式。break;case DLL_PROCESS_DETACH://這個DLL從程序的地址空間中解除對映。如果程序的終結是因為呼叫了TerminateProcess，系統就不會用DLL_PROCESS_DETACH來呼叫DLL的DllMain函式。這就意味著DLL在程序結束前沒有機會執行任何清理工作。break;return(TRUE);動態連結庫搜尋順序.對於Windows，載入動態連結庫時：Windows Desktop應用程式的DLL標準搜尋序：如果SafeDllSearchMode被禁止，則當前目錄成為第二個被搜尋的目錄。Windows Desktop應用程式的DLL替代搜尋序。這包括兩種情況：當所有DLL都完成搜尋，要開始執行DLL的初始化時，替代搜尋序結束，恢復標準搜尋序。執行時顯式連結.對於一個程序，每個被載入的DLL在程序的PEB中有一個引用計數器，在當前程序中每多一次載入該計數器便加一。codice_7 與 codice_8 指令影響每一個行程內含的計數器；動態連結則不影響。因此藉由呼叫 codice_8 而從記憶體解除安裝一 DLL 是很重要的。一個行程可以從它自己的 VAS 登出此計數器。該計數器變為0時，這個DLL從當前程序中解除安裝，但並不意味著會從物理記憶體中解除安裝，因為其它程序可能使用這個DLL。DLL 檔案能夠在執行時使用 codice_7。這些函式類似於 POSIX 標準 API 中的 codice_14、codice_15、和 codice_16。注意微軟簡單稱為“執行時動態連結”的執行時隱式連結，如果不能找到連結的 DLL 檔案，Windows 將提示一個錯誤訊息並且呼叫應用程式失敗。應用程式開發人員不能透過編譯連結來處理這種缺少 DLL 檔案的隱式連結問題，而且在更改了實現後還必須重新編譯連結整個程式——預設的增量連結模式會一直保留著那條缺少的函式引用，只有重新編譯連結才能去掉。相反，雖然顯式連結的程式碼量增多了，但開發人員有機會提供一個完善的出錯處理機制。執行時顯式連結的過程在所有語言中都是相同的，因為它依賴於 Windows API 而不是語言結構。只要一種語言能夠呼叫上述的 codice_7 等函式，就能執行執行時顯示連結。Win16下的DLL.16位Windows，所有程序共用同一個記憶體地址空間。如果一個DLL被多個程序使用，它只被載入到記憶體中一次，只有一份資料節。如果一個EXE檔案同時執行多次，那麼在記憶體中只有該EXE的一套只讀複製的記憶體起始地址。模組出自哪裡，是否已經載入到記憶體中。所以程序只能用例項控制代碼標識，而不能用模組控制代碼標識。Win32的程序使用自己專用的邏輯記憶體空間。程序的全域性變數不再是跨程序邊界可見的。例項控制代碼與模組控制代碼相同，都是指向模組載入後的記憶體基地址。這樣規定實際上也相容了Win16時例項控制代碼與模組控制代碼的含義。資源載入.EXE和DLL都有其自己的資源來找到DLL的資源。應用程式程序本身及其呼叫的每個DLL模組都具有一個全域性唯一的HINSTANCE控制代碼，它們代表了EXE或DLL模組在程序邏輯地址空間中的起始地址。程序本身的模組控制代碼一般為0x400000，而DLL模組預設載入地址為0x10000000。如果程式同時載入了多個DLL，則每個DLL模組都會有不同的HINSTANCE。幾種可行的辦法：方法1:// in MFC DLLvoid CDLL::ShowDlg(void)AFX_MANAGE_STATE(AfxGetStaticModuleState());CDialog dlg(IDD_DLL_DIALOG); //開啟ID為2000的對話方塊dlg.DoModal();AFX_MANAGE_STATE(AfxGetStaticModuleState());必須作為介面函式的第一條語句。功能是在棧上建立一個AFX_MAINTAIN_STATE2類的例項，利用其建構函式和解構函式對_afxThreadState.GetData()所指向記憶體儲存塊的模組狀態及恢復現場。方法2：// in MFC DLLvoid CDLL::ShowDlg(void)HINSTANCE save_hInstance = AfxGetResourceHandle(); //當前資源控制代碼AfxSetResourceHandle(theApp.m_hInstance); //設定為當前DLL的例項控制代碼所對應的資源控制代碼CDialog dlg(IDD_DLL_DIALOG); //開啟指定ID的對話方塊dlg.DoModal();AfxSetResourceHandle(save_hInstance);方法3：// in EXEvoid CEXE::OnButtonClick()HINSTANCE exe_hInstance = GetModuleHandle(NULL);HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll");AfxSetResourceHandle(dll_hInstance); //切換狀態ShowDlg();AfxSetResourceHandle(exe_hInstance); //恢復狀態編譯器和語言考慮.Delphi.在原始檔的開頭使用關鍵詞codice_18而不是codice_19，在檔案的末尾輸出函式使用codice_20排列。Delphi不需要codice_21檔案以從DLL中輸入函式。為了連結一個DLL，在函式宣告中使用關鍵詞codice_22。微軟 Visual Basic 及 .Net 框架.在Visual Basic中只支援執行時連結；但是除了使用codice_7和codice_12這兩個API函式之外，允許使用輸入函式的宣告來引入DLL函式，如果找不到codice_25檔案，VB將產生一個執行時異常。開發人員可以捕獲該異常並且進行適當的處理。Visual Basic 6這樣的較老的語言，只能呼叫codice_26呼叫約定修飾的函式，並需要在宣告DLL的輸出函式時使用Alias，否則就會出現在DLL中找不到函式入口點的錯誤。例如：Public Declare Function test2 Lib "PackingDLL.dll" Alias "_test2@4" (ByVal param As Integer) As Integer也可以用def檔案來定義dll輸出函式的名字。。另外需要注意，VB6呼叫的dll，如果還依賴其他的dll，那麼這些間接依賴的dll必須在VB6程式的dll搜尋路徑上，否則會報無法找到dll的錯誤。為了相容，codice_27 的方式一直沿用到 VB.net。不過在 .net 中，平臺呼叫提供了一種新的宣告DLL中的輸出函式方式，透過 codice_28 提供。例如：Public Function MessageBox(hWnd As Integer, lpText As String, lpCaption As String, uType As UInteger) As IntegerEnd Function在同為 .net 語言的 C# 中，這是僅有的處理方式。當然託管 C/C++ 就不必受此限制，見下節。C和C++.微軟Visual C++宣告為輸入或輸出；這種做法已經被Windows平臺上其他的C和C++編譯器採納，包括Windows平臺上的GCC。這種擴充套件在函式宣告前使用codice_29屬性：如果是遵從C命名規範不能識別C命名規範，那麼需要採取辦法指出在DLL中輸出函式的名字。也可以透過DEF檔案來定義輸出函式的名字與序號。或者使用如下的連結指令來制定dll輸出函式名字：#pragma comment(linker, "/export:add=@add@8")除了使用codice_29屬性定義輸入輸出函式之外，它們也可以列在專案codice_38檔案的IMPORT或者EXPORTS部分。codice_38檔案不是由編譯器進行處理，而是由連結器據此生成DLL檔案中輸出函式的名字與順序，這樣DEF檔案就不是C/C++特有的，其它語言寫的程式如果想要編譯為DLL也可以使用DEF。DLL的編譯將生成codice_25和codice_21兩個檔案。codice_21檔案被稱為輸入庫 API函式進行顯式載入DLL，用codice_12 API函式透過函式名稱獲取其載入後的記憶體地址、透過codice_8解除安裝DLL。codice_25一般說來必須放在PATH環境變數、預設系統路經或者是使用它的程式所在路徑三個的一個之內。COM伺服器DLL使用regsvr32.exe註冊，它將DLL的路徑和全域性唯一身份記錄在登錄檔中。應用程式能夠透過在登錄檔中查詢GUID、找到它的路徑從而使用這個DLL。程式設計例項.建立DLL輸出函式.下面的例子展示了與特定語言相關的從DLL輸出符號表的方法。Delphilibrary Example;// Function that adds two numbersfunction AddNumbers(a, b: Double): Double; cdecl;beginAddNumbers := a + bend;// Export this functionexportsAddNumbers;// DLL initialization code: no special handling neededbeginend.C 或 C++#include// Export this functionextern "C" __declspec(dllexport) double AddNumbers(double a, double b);// DLL initialization functionBOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)return TRUE;// Function that adds two numbersdouble AddNumbers(double a, double b)return a + b;使用DLL輸入.下面的例子展示了與特定語言相關的如何在編譯時連結DLL輸入符號表的方法。program Example;// Import function that adds two numbersfunction AddNumbers(a, b: Double): Double; cdecl; external 'Example.dll';var result: Double;beginresult := AddNumbers(1, 2);Writeln('The result was: ', result)end.C 或 C++#include#include// Import function that adds two numbersextern "C" __declspec(dllimport) double AddNumbers(double a, double b);int main(int argc, char **argv)double result = AddNumbers(1, 2);printf("The result was: %f\n", result);return 0;執行時使用顯式呼叫.下面的例子展示瞭如何使用不同語言特有的WIN32 API繫結進行執行時的呼叫和連結。Microsoft Visual BasicOption ExplicitDeclare Function AddNumbers Lib "Example.dll" (ByVal a As Double, ByVal b As Double) As DoubleSub Main()Dim Result As DoubleResult = AddNumbers(1, 2)Debug.Print "The result was: " &amp; ResultEnd SubC 或 C++#include#include// DLL function signaturetypedef double (*importFunction)(double, double);int main(int argc, char **argv)importFunction addNumbers;double result;// Load DLL fileHINSTANCE hinstLib = LoadLibrary("Example.dll");if (hinstLib == NULL) {printf("ERROR: unable to load DLL\n");return 1;// Get function pointeraddNumbers = (importFunction)GetProcAddress(hinstLib, "AddNumbers");if (addNumbers == NULL) {printf("ERROR: unable to find DLL function\n");return 1;// Call function.result = addNumbers(1, 2);// Unload DLL fileFreeLibrary(hinstLib);// Display resultprintf("The result was: %f\n", result);return 0;元件物件模型.元件物件模型將DLL概念擴充到了面向物件程式設計。物件能夠從另外一個程序呼叫或者在另外一臺機器上執行。COM物件有一個唯一的GUID並且能夠實現強大的後臺以簡化如Visual Basic和ASP這樣的GUI前臺應用。它們也可以使用指令碼語言程式設計。COM物件的建立和使用比DLL更為複雜。