動態連結函式庫

**動態連結函式庫**（英語：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) 內容，顯式指定哪個節是跨處理程序共享的。預設情況下，DLL 的資料節都是寫時複製（COW）。

具體說，DLL 裝入時需考慮下述情形：

1. 局部變數 —— 每個執行緒都有自己的棧，DLL 內部的局部變數隨所在函式被執行而在各自執行緒的呼叫棧上開闢儲存空間。
2. 全域變數
  1. DLL 內部定義的全域變數
    1. const 全域變數 —— 放入 const 節中，但不是各個處理程序共享；因為處理程序載入 DLL 時會初始化唯讀全域變數的值，這個值由可能是依賴於所在的處理程序，如 DLL 的函式在該處理程序中的邏輯位址。
    2. 非 const 全域變數 —— 放入各個處理程序各自專用的 data 節中。即 DLL 裝入時各個處理程序複製一份自己專用的 DLL 的 data 節。但是，對於一個處理程序內的多個執行緒並行存取這種處理程序空間全域變數，仍然存在執行緒安全問題。例如，在一個 COM 的 DLL 載入入一個處理程序的空間後，該處理程序的多個執行緒可能會並行存取該 COM 函式庫的 COM 物件。為此，Windows 與 COM 引入了執行緒「套間」(apartment) 技術。一個處理程序內，應用程式與載入的各個 DLL 分屬於不同的 Module，如果 DLL 使用所在 Module 的全域變數，例如動態連結 MFC 的 regular dll 在存取自己的 MFC 全域變數時，應該明確聲明。
  2. 存取 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。

### 符號解析和繫結

DLL 輸出的每個函式都由一個數字序號唯一標識，也可以由可選的名字標識。同樣，DLL 引入的函式也可以由序號或者名字標識。對於內部函式來說，只輸出序號的情形很常見。對於大多數視窗 API 函式來說名字是不同視窗版本之間保留不變的；序號有可能會發生變化。這樣，我們不能根據序號參照視窗 API 函式。

按照序號參照函式並不一定比按照名字參照函式效能更好：DLL 輸出表是按照名字排列的，所以對半尋找可以用來在在這個表中根據名字尋找這個函式。另外一方面，只有線性尋找才可以用於根據序號尋找函式。

將一個可執行檔**繫結**到一個特定版本的 DLL 也是可能的，這也就是說，可以在編譯時解析輸入函式（imported functions）的位址。對於繫結的輸入函式，連結工具儲存了輸入函式繫結的 DLL 的時間戳和校驗和。在執行時 Windows 檢查是否正在使用同樣版本的函式庫，如果是的話，Windows 將繞過處理輸入函式；否則如果函式庫與繫結的函式庫不同，Windows 將按照正常的方式處理輸入函式。

繫結的可執行檔如果執行在與它們編譯所用的環境一樣，函式呼叫將會較快，如果是在一個不同的環境它們就等同於正常的呼叫，所以繫結輸入函式沒有任何的缺點。例如，所有的標準 Windows 應用程式都繫結到它們各自的 Windows 發布版本的系統 DLL。將一個應用程式輸入函式繫結到它的目的環境的好機會是在應用程式安裝的過程。

### 執行時通知 DLL 處理程序 / 執行緒載入

處理程序 / 執行緒載入時，可以透過 DllMain 函式通知 DLL 相關資訊，提供對應處理的機會。

### 動態連結函式庫搜尋順序

對於 Windows，載入動態連結函式庫時：

* 如果記憶體中已經有同 module 名的 DLL，除非是 DLL redirection 或 manifest，否則直接就用記憶體中這個 DLL 而不再搜尋。
* 如果 DLL 名字屬於當前 Windows 版本的 Known DLL，則必須用 Known DLL。清單見 HKEY\_LOCAL\_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs.
* 如果 DLL 有依賴 DLL，作業系統按預設標準規則根據 module 名字搜尋依賴 DLL。即使第一個 DLL 指定了全路徑。

Windows Desktop 應用程式的 DLL 標準搜尋序：

1. 應用程式所在目錄；
2. 系統目錄。GetSystemDirectory 函式返回該目錄。
3. 16 位元系統目錄；
4. Windows 目錄。使用 GetWindowsDirectory 函式返回該目錄。
5. 當前（工作）目錄；
6. 環境變數 PATH 中列出的目錄。

如果 SafeDllSearchMode 被禁止，則當前目錄成為第二個被搜尋的目錄。

Windows Desktop 應用程式的 DLL 替代搜尋序。這包括兩種情況：

* LoadLibraryEx 函式使用參數 LOAD\_WITH\_ALTERED\_SEARCH\_PATH：LoadLibraryEx 函式不是從應用程式所在目錄開始，而是從參數 lpFileName（即要載入的 dll）所在目錄開始搜尋所有依賴 DLL。
* 用 SetDllDirectory 函式修改搜素序。

當所有 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` 等函式，就能執行執行時顯示連結。

### Win16 下的 DLL

16 位元 Windows，所有處理程序共享同一個記憶體位址空間。如果一個 DLL 被多個處理程序使用，它只被載入到記憶體中一次，只有一份資料節（data segment）。也就是說，DLL 是系統全域而不是處理程序內的；處理程序不能得到 DLL 的一份獨立的拷貝。每個 DLL 在記憶體中只有一份實例（instance）。

如果一個 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:

AFX\_MANAGE\_STATE (AfxGetStaticModuleState ()); 必須作為介面函式的第一條語句。功能是在棧上建立一個 AFX\_MAINTAIN\_STATE2 類別的實例，利用其建構函式和解構函式對 \_afxThreadState.GetData () 所指向記憶體儲存塊的模組狀態（AFX\_MODULE\_STATE 類型）設定現場（AfxGetModuleState 函式返回的值）及恢復現場。

方法 2：

方法 3：

## 編譯器和語言考慮

### Delphi

在原始檔的開頭使用關鍵詞 `library` 而不是 `program`，在檔案的末尾輸出函式使用 `exports` 排列。

Delphi 不需要 `LIB` 檔案以從 DLL 中輸入函式。為了連結一個 DLL，在函式聲明中使用關鍵詞 `external`。

### 微軟 Visual Basic 及 .Net 框架

在 Visual Basic（VB）中只支援執行時連結；但是除了使用 `LoadLibrary` 和 `GetProcAddress` 這兩個 API 函式之外，允許使用輸入函式的**聲明**來引入 DLL 函式，如果找不到 `DLL` 檔案，VB 將產生一個執行時異常。開發人員可以擷取該異常並且進行適當的處理。

Visual Basic 6 這樣的較老的語言，只能呼叫 `__stdcall` 呼叫約定修飾的函式，並需要在聲明 DLL 的輸出函式時使用 Alias，否則就會出現在 DLL 中找不到函式入口點的錯誤。例如：

也可以用 def 檔案來定義 dll 輸出函式的名字。。另外需要注意，VB6 呼叫的 dll，如果還依賴其他的 dll，那麼這些間接依賴的 dll 必須在 VB6 程式的 dll 搜尋路徑上，否則會報無法找到（被直接依賴的）dll 的錯誤。

為了相容，`Declare Function` 的方式一直沿用到 VB.net。不過在 .net 中，**平台呼叫**提供了一種新的聲明 DLL 中的輸出函式方式，透過 `System.Runtime.InteropServices.DllImportAttribute` 提供。例如：

在同為 .net 語言的 C# 中，這是僅有的處理方式。當然代管 C/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 中輸出的變數。例如，DLL 輸出一個 C++ 類別，該類有一個靜態變數，那麼在外部檔案使用這個類時必須用 `__declspec(dllimport)` 聲明。

如果是遵從 C 命名規範（naming convention）的外部名字，它們必須在 C++ 程式碼中聲明為 `extern "C"` 以避免它們使用 C++ 命名規範。如果使用 dll 的語言（如 Fortran、Visual Basic 6）不能辨識 C 命名規範，那麼需要採取辦法指出在 DLL 中輸出函式的名字。也可以透過 DEF 檔案來定義輸出函式的名字與序號。或者使用如下的連結指令來制定 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 輸出函式

下面的例子展示了與特定語言相關的從 DLL 輸出符號表的方法。

**Delphi**

**C 或 C++**

### 使用 DLL 輸入

下面的例子展示了與特定語言相關的如何在編譯時連結 DLL 輸入符號表的方法。

**Delphi**

C 或 C++

### 執行時使用顯式呼叫

下面的例子展示了如何使用不同語言特有的 WIN32 API 繫結進行執行時的呼叫和連結。

**Microsoft Visual Basic**

**C 或 C++**

## 組件物件模型

組件物件模型（COM）將 DLL 概念擴充到了物件導向程式設計。物件能夠從另外一個處理程序呼叫或者在另外一台機器上執行。COM 物件有一個唯一的 GUID 並且能夠實現強大的後台以簡化如 Visual Basic 和 ASP 這樣的 GUI 前台套用。它們也可以使用手稿語言編程。COM 物件的建立和使用比 DLL 更為複雜。

## 參見

* Dependency Walker, 顯示 DLL 和 EXE 檔案輸出和參照函式的工具
* Dynamic Library
* Library Linking (Computer Science)
* 連結器
* 載入器
* 目的碼
* Shared Library
* 靜態連結函式庫
* DLL 地獄

## 備註

## 外部連結

* Dll Download （頁面存檔備份，存於網際網路檔案館） 自由的 Windows 和第三方軟體擴展中用到的 Dll 下載
* \_\_declspec C++ Language Reference on MSDN
* dllexport, dllimport on MSDN
* Dynamic-Link Libraries on MSDN
* Dynamic-Link Library Functions on MSDN
* Microsoft Portable Executable and Common Object File Format Specification（頁面存檔備份，存於網際網路檔案館）
* Win32 DLL （頁面存檔備份，存於網際網路檔案館） www.functionx.com 上的 DLL 製作和使用教程
* Delay Load Dlls Error Recovery on www.codemaestro.com.
* Dll Download and Repair on www.mydll.org

## 參考文獻

* Hart, Johnson. _Windows System Programming Third Edition_. Addison-Wesley, 2005. ISBN 0-321-25619-0
* Rector, Brent et al. _Win32 Programming_. Addison-Wesley Developers Press, 1997. ISBN 0-201-63492-9.