複製鏈接
請複製以下鏈接發送給好友

動態鏈接庫

(編程相關名詞)

鎖定
動態鏈接庫(Dynamic Link Library 或者 Dynamic-link Library,縮寫為 DLL),是微軟公司在微軟Windows操作系統中,實現共享函數庫概念的一種方式。這些庫函數的擴展名是 ”.dll、.ocx(包含ActiveX控制的庫)或者 .drv(舊式的系統驅動程序)。
中文名
動態鏈接庫
外文名
Dynamic Link Library
縮    寫
DLL
類    別
鏈接庫

動態鏈接庫介紹

動態鏈接提供了一種方法,使進程可以調用不屬於其可執行代碼的函數。函數的可執行代碼位於一個 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問題當作自己的目標,它允許同一個共享庫的不同版本並列共存。由於現代的計算機有足夠的磁盤空間和內存,這也可以作為一個合理的實現方法 [1] 

動態鏈接庫特徵

內存管理
Win32中,DLL文件按照片段(sections)進行組織。每個片段有它自己的屬性,如可寫或是隻讀、可執行(代碼)或者不可執行(數據)等等。這些section可分為兩種,一個是與絕對地址尋址無關的,所以能被多進程公用;另一個是與絕對地址尋址有關的,這個就必須由每個進程有自己的副本專用。sections的這種二分類,在編譯DLL時就已經由編譯器、鏈接器給標註好了。所以在裝入DLL時,裝入器知道哪些sections在內存物理地址空間只需要有一份,供多個進程共用(映射到各個進程的內存邏輯地址空間,所以邏輯地址可以不同); 哪些sections必須是進程使用自己的專用副本。
具體説,DLL裝入時需考慮下述情形:
  1. 局部變量——每個線程都有自己的棧,DLL內部的局部變量隨所在函數被執行而在各自線程的調用棧上開闢存儲空間。
  2. 全局變量
    1. const全局變量——放入const節中,多進程共享;
    2. 非const全局變量——放入各個進程各自專用的data節中。即DLL裝入時各個進程複製一份自己專用的DLL的data節。但是,對於一個進程內的多個線程併發訪問這種進程空間全局變量,仍然存在線程安全問題。例如,在一個COM的DLL加載入一個進程的空間後,該進程的多個線程可能會併發訪問該COM庫的COM對象。為此,Windows與COM引入了線程“套間”(apartment)技術。一個進程內,應用程序與加載的各個DLL分屬於不同的Module,如果DLL使用所在Module的全局變量,例如動態鏈接MFC的regular dll在訪問自己的MFC全局變量時,應該明確聲明。
    3. DLL內部定義的全局變量
    4. 訪問DLL以外定義的全局變量——使用間址技術,在DLL的data節中用一個指針數據類型的內存空間來保存一個外部全局變量的地址。
  3. 函數調用
    1. 調用DLL內部定義的函數。這不是問題。
    2. 調用DLL外部定義的函數。例如,DLL內部調用一個外部函數foo()。這個foo函數在進程1中可能實現為“四捨五入”,在進程2中實現為“下取整”。所以調用外部函數是各個進程私用的事情。解決辦法是使用間址技術,在data節中用一個“函數指針”數據類型的內存空間來保存這種外部函數的入口地址。
  4. 跳轉指令
    1. DLL內部跳轉,不是問題
    2. 跳轉到DLL外部,解決同上述3.2
DLL代碼段通常被使用這個DLL的所有進程所共享。如果代碼段所佔據的物理內存被收回,它的內容就會被放棄,後面如果需要的話就直接從DLL文件重新加載。
與代碼段不同,DLL的數據段通常是私有的;也就是説,每個使用DLL的進程都有自己的DLL數據副本。作為選擇,數據段可以設置為共享,允許通過這個共享內存區域進行進程間通信。但是,因為用户權限不能應用到這個共享DLL內存,這將產生一個安全漏洞;也就是一個進程能夠破壞共享數據,這將導致其它的共享進程異常。例如,一個使用訪客賬號的進程將可能通過這種方式破壞其它運行在特權賬號的進程。這是在DLL中避免使用共享片段的一個重要原因。
當DLL被如UPX這樣一個可執行的packer壓縮時,它的所有代碼段都標記為可以讀寫並且是非共享的。可以讀寫的代碼段,類似於私有數據段,是每個進程私有的並且被頁面文件備份。這樣,壓縮DLL將同時增加內存和磁盤空間消耗,所以共享DLL應當避免使用壓縮DLL [2] 
符號解析和綁定
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 API而不是語言結構 [3] 

動態鏈接庫優點

  1. 擴展了應用程序的特性;
  2. 可以用許多種編程語言來編寫;
  3. 簡化了軟件項目的管理;
  4. 有助於節省內存;
  5. 有助於資源共享;
  6. 有助於應用程序的本地化;
  7. 有助於解決平台差異;
  8. 可以用於一些特殊的目的。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 加載函數或終止函數。例如,在入口點函數中,不應直接或間接調用 LoadLibrary 函數或LoadLibraryEx 函數。此外,不應在進程終止時調用 FreeLibrary函數。
注意:在多線程應用程序中,請確保將對 DLL 全局數據的訪問進行同步(線程安全),以避免可能的數據損壞。為此,請使用 TLS 為各個線程提供唯一的數據。

動態鏈接庫如何導出

要導出 DLL 函數,您可以嚮導出的 DLL 函數中添加函數關鍵字,也可以創建模塊定義文件(.def) 以列出導出的 DLL 函數。
(1)嚮導出的 DLL 函數中添加函數關鍵字
要使用函數關鍵字,您必須使用以下關鍵字來聲明要導出的各個函數:
__declspec(dllexport)
要在應用程序中使用導出的 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語言無法識別函數 [4] 
下面的代碼是一個定義文件的示例。
// SampleDLL.def
//
LIBRARY "SampleDLL"
EXPORTS

動態鏈接庫示例

HelloWorld 示例 DLL 和應用程序
在 MicrosoftVisual C++6.0 中,可以通過選擇“Win32動態鏈接庫”項目類型或“MFC應用程序嚮導(dll)”來創建 DLL。下面的代碼是一個在 Visual C++ 中通過使用“Win32 動態鏈接庫”項目類型創建的 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
ODBC功能的DLL。
參考資料