動態鏈接庫

動態鏈接庫(英語:Dynamic-link library,縮寫為DLL)是微軟公司Windows 操作系統中實現共享函數庫概念的一種實作方式。這些庫函數的擴展名.DLL.OCX(包含ActiveX控制的函式庫)或者.DRV(舊式的系統驅動程序)。

所謂動態鏈接,就是把一些經常會共用的程式碼(靜態鏈接的OBJ英語Object file程式庫)製作成DLL檔,當執行檔呼叫到DLL檔內的函數時,Windows作業系統才會把DLL檔載入記憶體內,DLL檔本身的結構就是可執行檔,當程式有需求時函數才進行鏈接。透過動態鏈接方式,記憶體浪費的情形將可大幅降低。靜態鏈接庫則是直接連結到執行檔。

DLL的文件格式與視窗EXE文件一樣——也就是說,等同於32位視窗的可移植執行文件(PE)和16位視窗的New Executable英語New Executable(NE)。作為EXE格式,DLL可以包括原始碼數據資源的多種組合。

在更廣泛的意義上說,任何同樣檔案格式電腦檔案都可以稱作資源DLL。這樣的DLL的例子有擴展名為ICL圖標函式庫、擴展名為FONFOT字型檔案。

背景

DLL的最初目的是節約應用程序所需的磁盤和內存空間。在一個傳統的非共享函式庫中,一部分代碼簡單地附加到調用的程序上。如果兩個程序調用同一個子程序,就會出現兩份那段代碼。相反,許多應用共享的代碼能夠切分到一個DLL中,在硬盤上存為一個文件,在內存中使用一個實例(instance)。DLL的廣泛應用使得早期的視窗能夠在緊張的內存條件下運行。

DLL提供了如模組化這樣的共享函式庫的普通好處。模塊化允許僅僅更改幾個應用程序共享使用的一個DLL中的代碼和數據而不需要更改應用程序自身。這種模塊化的基本形式允許如Microsoft OfficeMicrosoft 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裝入時需考慮下述情形:

  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相關信息,提供對應處理的機會。

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,加載動態鏈接庫時:

  • 如果內存中已經有同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中有一個引用計數器,在當前進程中每多一次加載該計數器便加一。LoadLibraryFreeLibrary 指令影響每一個行程內含的計數器;動態連結則不影響。因此藉由呼叫 FreeLibrary 而從記憶體卸載一 DLL 是很重要的。一個行程可以從它自己的 VAS 註銷此計數器。該計數器變為0時,這個DLL從當前進程中卸載,但並不意味着會從物理內存中卸載,因為其它進程可能使用這個DLL。

DLL 文件能夠在運行時使用 LoadLibrary(或者 LoadLibraryEx)API 函數進行顯式調用,這個的過程微軟簡單地稱為運行時動態調用。如果在上述API的參數中指明了庫文件的全路徑,則不再搜索這個庫文件。API 函數GetProcAddress 查找具有某名稱的輸出函數、FreeLibrary卸載 DLL(實際上是引用計數器減一)。這些函數類似於 POSIX 標準 API 中的 dlopendlsym、和 dlclose

注意微軟簡單稱為「運行時動態鏈接」的運行時隱式鏈接,如果不能找到鏈接的 DLL 文件,Windows 將提示一個錯誤消息並且調用應用程序失敗(準確地說是不能創建進程)。應用程序開發人員不能通過編譯鏈接來處理這種缺少 DLL 文件的隱式鏈接問題,而且在更改了實現後還必須重新編譯鏈接整個程序——默認的增量鏈接模式會一直保留着那條缺少的函數引用,只有重新編譯鏈接才能去掉。相反,雖然顯式鏈接的代碼量增多了,但開發人員有機會提供一個完善的出錯處理機制。

運行時顯式鏈接的過程在所有語言中都是相同的,因為它依賴於 Windows API 而不是語言結構。只要一種語言能夠調用上述的 LoadLibrary 等函數,就能執行運行時顯示鏈接。

Win16下的DLL

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)中只支持運行時鏈接;但是除了使用LoadLibraryGetProcAddress這兩個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++ 就不必受此限制,見下節。

CC++

微軟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的語言(如FortranVisual 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的編譯將生成DLLLIB兩個文件。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

 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輸入

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

Delphi
 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更為複雜。

參見

備註

  1. ^ 如果不使用__declspec(dllimport),那麼導入的動態鏈接庫全局變量,諸如extern int dllGlobalVar;, 實際上是該變量的指針值。因此在使用它時,必須:printf("%d ", *(int*)dllGlobalVar); *(int*)dllGlobalVar = 1; 也可以使用GetProcAddress函數來顯示導入一個DLL定義的全局輸出變量,例如:int &my_int = *(int*)GetProcAddress(hInstLibrary, "DLLData");

外部連結

參考文獻

  • 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.
  1. ^ /SECTION (Specify Section Attributes). [2019-11-27]. (原始內容存檔於2020-08-20). 
  2. ^ Raymond Chen:“What is the difference between HINSTANCE and HMODULE?”,in 《old new thing》,MSDN,,June 14, 2004. [2022-01-25]. (原始內容存檔於2019-03-06). 
  3. ^ 3.0 3.1 MSDN:Exporting from a DLL Using DEF Files