Loading AI tools
来自维基百科,自由的百科全书
動態連結函式庫(英語:Dynamic-link library,縮寫為DLL)是微軟公司在 Windows 作業系統中實現共享函式庫概念的一種實作方式。這些庫函式的副檔名是.DLL
、.OCX
(包含ActiveX控制的函式庫)或者.DRV
(舊式的系統驅動程式)。
所謂動態連結,就是把一些經常會共享的程式碼(靜態連結的OBJ程式庫)製作成DLL檔,當執行檔呼叫到DLL檔內的函式時,Windows作業系統才會把DLL檔載入記憶體內,DLL檔本身的結構就是可執行檔,當程式有需求時函式才進行連結。透過動態連結方式,記憶體浪費的情形將可大幅降低。靜態連結函式庫則是直接連結到執行檔。
DLL的檔案格式與視窗EXE檔案一樣——也就是說,等同於32位元視窗的可移植執行檔案(PE)和16位元視窗的New Executable(NE)。作為EXE格式,DLL可以包括原始碼、資料和資源的多種組合。
在更廣泛的意義上說,任何同樣檔案格式的電腦檔案都可以稱作資源DLL。這樣的DLL的例子有副檔名為ICL
的圖示函式庫、副檔名為FON
和FOT
的字型檔案。
DLL的最初目的是節約應用程式所需的磁碟和主記憶體空間。在一個傳統的非共享函式庫中,一部份程式碼簡單地附加到呼叫的程式上。如果兩個程式呼叫同一個子程式,就會出現兩份那段程式碼。相反,許多套用共享的程式碼能夠切分到一個DLL中,在硬碟上存為一個檔案,在主記憶體中使用一個實例(instance)。DLL的廣泛套用使得早期的視窗能夠在緊張的主記憶體條件下執行。
DLL提供了如模組化這樣的共享函式庫的普通好處。模組化允許僅僅更改幾個應用程式共享使用的一個DLL中的程式碼和資料而不需要更改應用程式自身。這種模組化的基本形式允許如Microsoft Office、Microsoft Visual Studio、甚至Microsoft Windows自身這樣大的應用程式使用較為緊湊的修補程式和服務包。
模組化的另外一個好處是外掛程式的通用介面使用。單個的介面允許舊的模組與新的模組一樣能夠與以前的應用程式執行時無縫地整合到一起,而不需要對應用程式本身作任何更改。這種動態擴充的思想在ActiveX中發揮到了極致。
儘管有這麼多的優點,使用DLL也有一個缺點:DLL地獄,也就是幾個應用程式在使用同一個共享DLL函式庫發生版本衝突。這樣的衝突可以透過將不同版本的問題DLL放到應用程式所在的資料夾而不是放到系統資料夾來解決;但是,這樣將抵消共享DLL節約的空間。目前,Microsoft .NET將解決DLL hell問題當作自己的目標,它允許同一個共享函式庫的不同版本並列共存(WinSxS)。由於現代的電腦有足夠的磁碟空間和主記憶體,這也可以作為一個合理的實現方法。
在Win32中,DLL檔案按照片段(sections)進行組織。每個片段有它自己的內容,如可寫或是唯讀、可執行(程式碼)或者不可執行(資料)等等。這些section可分為兩種,一個是與絕對位址定址無關的,所以能被多處理程序公用;另一個是與絕對位址定址有關的,這個就必須由每個處理程序有自己的副本專用。sections的這種二分類,在編譯DLL時就已經由編譯器、連結器給標註好了。所以在裝入DLL時,裝入器知道哪些sections在主記憶體實體位址空間只需要有一份,供多個處理程序共享(對映到各個處理程序的主記憶體邏輯位址空間,所以邏輯位址可以不同); 哪些sections必須是處理程序使用自己的專用副本。
也可在程式編譯時透過編譯選項/section 的S (Shared)內容,顯式指定哪個節是跨處理程序共享的。[1]預設情況下,DLL的資料節都是寫時複製(COW)。
具體說,DLL裝入時需考慮下述情形:
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。將一個應用程式輸入函式繫結到它的目的環境的好機會是在應用程式安裝的過程。
處理程序/執行緒載入時,可以透過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来结束线程(线程函数返回时,系统也会自动调用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中有一個參照計數器,在當前處理程序中每多一次載入該計數器便加一。LoadLibrary
與 FreeLibrary
指令影響每一個行程內含的計數器;動態連結則不影響。因此藉由呼叫 FreeLibrary
而從記憶體移除一 DLL 是很重要的。一個行程可以從它自己的 VAS 註銷此計數器。該計數器變為0時,這個DLL從當前處理程序中移除,但並不意味著會從實體記憶體中移除,因為其它處理程序可能使用這個DLL。
DLL 檔案能夠在執行時使用 LoadLibrary
(或者 LoadLibraryEx
)API 函式進行顯式呼叫,這個的過程微軟簡單地稱為執行時動態呼叫。如果在上述API的參數中指明了函式庫檔案的全路徑,則不再搜尋這個函式庫檔案。API 函式GetProcAddress
尋找具有某名稱的輸出函式、FreeLibrary
移除 DLL(實際上是參照計數器減一)。這些函式類似於 POSIX 標準 API 中的 dlopen
、dlsym
、和 dlclose
。
注意微軟簡單稱為「執行時動態連結」的執行時隱式連結,如果不能找到連結的 DLL 檔案,Windows 將提示一個錯誤訊息並且呼叫應用程式失敗(準確地說是不能建立處理程序)。應用程式開發人員不能透過編譯連結來處理這種缺少 DLL 檔案的隱式連結問題,而且在更改了實現後還必須重新編譯連結整個程式——預設的增量連結模式會一直保留著那條缺少的函式參照,只有重新編譯連結才能去掉。相反,雖然顯式連結的程式碼量增多了,但開發人員有機會提供一個完善的出錯處理機制。
執行時顯式連結的過程在所有語言中都是相同的,因為它依賴於 Windows API 而不是語言結構。只要一種語言能夠呼叫上述的 LoadLibrary
等函式,就能執行執行時顯示連結。
16位元Windows,所有處理程序共享同一個主記憶體位址空間。如果一個DLL被多個處理程序使用,它只被載入到主記憶體中一次,只有一份資料節(data segment)。也就是說,DLL是系統全域而不是處理程序內的;處理程序不能得到DLL的一份獨立的拷貝。每個DLL在主記憶體中只有一份實例(instance)。[2]
如果一個EXE檔案同時執行多次,那麼在主記憶體中只有該EXE的一套唯讀拷貝(如程式碼或資源),但這個EXE的每個處理程序都有自己的一套資料段,即有多份實例(instance)。實際上,處理程序的實例控制代碼就是data segment(存放處理程序的全域變數)的主記憶體起始位址。
模組(module)是指一個硬碟檔案,可被載入到主記憶體中。模組控制代碼是一個資料結構,表示這個硬碟檔案的各部份(section)出自哪裡,是否已經載入到主記憶體中。
所以處理程序只能用實例控制代碼標識,而不能用模組控制代碼標識。
Win32的處理程序使用自己專用的邏輯主記憶體空間。處理程序的全域變數不再是跨處理程序邊界可見的。實例控制代碼與模組控制代碼相同,都是指向模組載入後的主記憶體基位址。這樣規定實際上也相容了Win16時實例控制代碼與模組控制代碼的含義。
EXE和DLL都有其自己的資源(如對話方塊資源),而且這些資源的ID可能重複,預設使用EXE的資源。如果需要載入、使用DLL中的資源,需要透過DLL載入後的實例控制代碼(HINSTANCE)來找到DLL的資源。
應用程式處理程序本身及其呼叫的每個DLL模組都具有一個全域唯一的HINSTANCE控制代碼,它們代表了EXE或DLL模組在處理程序邏輯位址空間中的起始位址。處理程序本身的模組控制代碼一般為0x400000,而DLL模組預設載入位址為0x10000000。如果程式同時載入了多個DLL,則每個DLL模組都會有不同的HINSTANCE。
幾種可行的辦法:
方法1:
// in MFC DLL
void 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()所指向主記憶體儲存塊的模組狀態(AFX_MODULE_STATE類型)設定現場(AfxGetModuleState函式返回的值)及恢復現場。
方法2:
// in MFC DLL
void 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 EXE
void CEXE::OnButtonClick()
{
HINSTANCE exe_hInstance = GetModuleHandle(NULL);
HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll");
AfxSetResourceHandle(dll_hInstance); //切换状态
ShowDlg();
AfxSetResourceHandle(exe_hInstance); //恢复状态
}
在原始檔的開頭使用關鍵詞library
而不是program
,在檔案的末尾輸出函式使用exports
排列。
Delphi不需要LIB
檔案以從DLL中輸入函式。為了連結一個DLL,在函式聲明中使用關鍵詞external
。
在Visual Basic(VB)中只支援執行時連結;但是除了使用LoadLibrary
和GetProcAddress
這兩個API函式之外,允許使用輸入函式的聲明來引入DLL函式,如果找不到DLL
檔案,VB將產生一個執行時異常。開發人員可以擷取該異常並且進行適當的處理。
Visual Basic 6這樣的較老的語言,只能呼叫__stdcall
呼叫約定修飾的函式,並需要在聲明DLL的輸出函式時使用Alias,否則就會出現在DLL中找不到函式入口點的錯誤。例如:
Public Declare Function test2 Lib "PackingDLL.dll" Alias "_test2@4" (ByVal param As Integer) As Integer
也可以用def檔案來定義dll輸出函式的名字。[3]。另外需要注意,VB6呼叫的dll,如果還依賴其他的dll,那麼這些間接依賴的dll必須在VB6程式的dll搜尋路徑上,否則會報無法找到(被直接依賴的)dll的錯誤。
為了相容,Declare Function
的方式一直沿用到 VB.net。不過在 .net 中,平台呼叫提供了一種新的聲明DLL中的輸出函式方式,透過 System.Runtime.InteropServices.DllImportAttribute
提供。例如:
<DllImportAttribute("user32.dll", EntryPoint:="MessageBoxW", SetLastError:=True, CharSet:=CharSet.Unicode, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)>
Public Function MessageBox(hWnd As Integer, lpText As String, lpCaption As String, uType As UInteger) As Integer
End Function
在同為 .net 語言的 C# 中,這是僅有的處理方式。當然代管 C/C++ 就不必受此限制,見下節。
微軟Visual C++(MSVC)提供了許多標準C++的擴充,它允許直接在C++程式碼中將函式(類、資料變數)聲明為輸入或輸出;這種做法已經被Windows平台上其他的C和C++編譯器採納,包括Windows平台上的GCC。這種擴充在函式聲明前使用__declspec
內容:
_declspec(dllexport)
用於在DLL原始檔中聲明要輸出的C++類別、函式以及資料。_declspec(dllimport)
用於在外部程式聲明由DLL輸出的C++類別、函式以及資料。很多情況下不使用__declspec(dllimport)
也能正確編譯程式碼。但使用__declspec(dllimport)
使編譯器可以生成更好的程式碼,因為它可以確定函式是否存在於DLL中,這使得在跨DLL邊界的函式呼叫時編譯器可以生成跳過間接定址級別的程式碼。需要特別注意的是,必須使用__declspec(dllimport)
才能匯入DLL中輸出的變數。[註 1]例如,DLL輸出一個C++類別,該類有一個靜態變數,那麼在外部檔案使用這個類時必須用__declspec(dllimport)
聲明。如果是遵從C命名規範(naming convention)的外部名字,它們必須在C++程式碼中聲明為extern "C"
以避免它們使用C++命名規範。如果使用dll的語言(如Fortran、Visual Basic 6)不能辨識C命名規範,那麼需要採取辦法指出在DLL中輸出函式的名字。也可以透過DEF檔案來定義輸出函式的名字與序號。[3]或者使用如下的連結指令來制定dll輸出函式名字:
#pragma comment(linker, "/export:add=@add@8")
除了使用__declspec
內容定義輸入輸出函式之外,它們也可以列在專案DEF
檔案的IMPORT或者EXPORTS部份。DEF
檔案不是由編譯器進行處理,而是由連結器據此生成DLL檔案中輸出函式的名字與順序(ordinal number),這樣DEF檔案就不是C/C++特有的,其它語言寫的程式如果想要編譯為DLL也可以使用DEF。
DLL的編譯將生成DLL
和LIB
兩個檔案。LIB
檔案被稱為輸入函式庫(import library),在編譯時為呼叫DLL的程式提供「樁」(stub)實際上是間接跳轉到執行時載入的DLL的對應的函式上再繼續執行。這種透過輸入函式庫來使用DLL的方式,在程式執行時啟動處理程序時就會自動(隱式)載入所有用到的DLL。另一種使用DLL的方式是透過LoadLibrary
(或者LoadLibraryEx
) API函式進行顯式載入DLL,用GetProcAddress
API函式透過函式名稱取得其載入後的主記憶體位址、透過FreeLibrary
移除DLL。
DLL
一般說來必須放在PATH環境變數、預設系統路經或者是使用它的程式所在路徑三個的一個之內。COM伺服器DLL使用regsvr32.exe註冊,它將DLL的路徑和全域唯一身分(GUID)記錄在登錄檔中。應用程式能夠透過在登錄檔中尋找GUID、找到它的路徑從而使用這個DLL。
下面的例子展示了與特定語言相關的從DLL輸出符號表的方法。
Delphi
library Example;
// Function that adds two numbers
function AddNumbers(a, b: Double): Double; cdecl;
begin
AddNumbers := a + b
end;
// Export this function
exports
AddNumbers;
// DLL initialization code: no special handling needed
begin
end.
C 或 C++
#include <windows.h>
// Export this function
extern "C" __declspec(dllexport) double AddNumbers(double a, double b);
// DLL initialization function
BOOL APIENTRY DllMain(HANDLE hModule, [[DWORD]] dwReason, LPVOID lpReserved)
{
return TRUE;
}
// Function that adds two numbers
double AddNumbers(double a, double b)
{
return a + b;
}
下面的例子展示了與特定語言相關的如何在編譯時連結DLL輸入符號表的方法。
program Example;
{$APPTYPE CONSOLE}
// Import function that adds two numbers
function AddNumbers(a, b: Double): Double; cdecl; external 'Example.dll';
var result: Double;
begin
result := AddNumbers(1, 2);
Writeln('The result was: ', result)
end.
C 或 C++
#include <windows.h>
#include <stdio.h>
// Import function that adds two numbers
extern "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 Basic
Option Explicit
Declare Function AddNumbers Lib "Example.dll" (ByVal a As Double, ByVal b As Double) As Double
Sub Main()
Dim Result As Double
Result = AddNumbers(1, 2)
Debug.Print "The result was: " & Result
End Sub
C 或 C++
#include <windows.h>
#include <stdio.h>
// DLL function signature
typedef double (*importFunction)(double, double);
int main(int argc, char **argv)
{
importFunction addNumbers;
double result;
// Load DLL file
HINSTANCE hinstLib = LoadLibrary("Example.dll");
if (hinstLib == NULL) {
printf("ERROR: unable to load DLL\n");
return 1;
}
// Get function pointer
addNumbers = (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 file
FreeLibrary(hinstLib);
// Display result
printf("The result was: %f\n", result);
return 0;
}
組件對象模型(COM)將DLL概念擴充到了物件導向程式設計。對象能夠從另外一個處理程序呼叫或者在另外一台機器上執行。COM對象有一個唯一的GUID並且能夠實現強大的後台以簡化如Visual Basic和ASP這樣的GUI前台套用。它們也可以使用手稿語言編程。COM對象的建立和使用比DLL更為複雜。
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.