Win32環境下動態連結庫(DLL)程式設計原理 (轉)

worldblog發表於2007-12-05
Win32環境下動態連結庫(DLL)程式設計原理 (轉)[@more@]環境下動態連結庫(DLL)原理



李 欣

比較大應用都由很多模組組成,這些模組分別完成相對獨立的功能,它們彼此協作來完成整個的工作。其中可能存在一些模組的功能較為通用,在構造其它軟體系統時仍會被使用。在構造軟體系統時,如果將所有模組的都靜態編譯到整個應用程式EXE中,會產生一些問題:一個缺點是增加了應用程式的大小,它會佔用更多的空間,程式執行時也會消耗較大的空間,造成系統資源的浪費;另一個缺點是,在編寫大的EXE程式時,在每次修改重建時都必須調整編譯所有原始碼,增加了編譯過程的複雜性,也不利於階段性的單元測試。

系統平臺上提供了一種完全不同的較有效的程式設計和執行環境,你可以將獨立的程式模組建立為較小的DLL(Dynamic Linkable Library)檔案,並可對它們單獨編譯和測試。在執行時,只有當EXE程式確實要這些DLL模組的情況下,系統才會將它們裝載到記憶體空間中。這種方式不僅減少了EXE檔案的大小和對記憶體空間的需求,而且使這些DLL模組可以同時被多個應用程式使用。 Windows自己就將一些主要的系統功能以DLL模組的形式實現。例如IE中的一些基本功能就是由DLL檔案實現的,它可以被其它應用程式呼叫和整合。

一般來說,DLL是一種磁碟檔案(通常帶有DLL副檔名),它由全域性資料、服務和資源組成,在執行時被系統載入到程式的虛擬空間中,成為呼叫程式的一部分。如果與其它DLL之間沒有衝突,該檔案通常對映到程式虛擬空間的同一地址上。DLL模組中包含各種匯出函式,用於向外界提供服務。Windows在載入DLL模組時將程式函式呼叫與DLL檔案的匯出函式相匹配。
在Win32環境中,每個程式都複製了自己的讀/寫全域性變數。如果想要與其它程式共享記憶體,必須使用記憶體對映檔案或者宣告一個共享資料段。DLL模組需要的堆疊記憶體都是從執行程式的堆疊中分配出來的。
DLL現在越來越容易編寫。Win32已經大大簡化了其程式設計,並有許多來自AppWizard和MFC類庫的支援。

一、匯出和匯入函式的匹配

DLL檔案中包含一個匯出函式表。這些匯出函式由它們的符號名和稱為標識號的整數與外界聯絡起來。函式表中還包含了DLL中函式的地址。當應用程式載入DLL模組時時,它並不知道呼叫函式的實際地址,但它知道函式的符號名和標識號。動態連結過程在載入的DLL模組時動態建立一個函式呼叫與函式地址的對應表。如果重新編譯和重建DLL檔案,並不需要修改應用程式,除非你改變了匯出函式的符號名和引數序列。
簡單的DLL檔案只為應用程式提供匯出函式,比較複雜的DLL檔案除了提供匯出函式以外,還呼叫其它DLL檔案中的函式。這樣,一個特殊的DLL可以既有匯入函式,又有匯入函式。這並不是一個問題,因為動態連結過程可以處理交叉相關的情況。
在DLL程式碼中,必須像下面這樣明確宣告匯出函式:
__declspec(dllexport) int MyFunction(int n);
但也可以在模組定義(DEF)檔案中列出匯出函式,不過這樣做常常引起更多的麻煩。在應用程式方面,要求像下面這樣明確宣告相應的輸入函式:
__declspec(dllimport) int MyFuncition(int n);
僅有匯入和匯出宣告並不能使應用程式內部的函式呼叫連結到相應的DLL檔案上。應用程式的專案必須為連結程式指定所需的輸入庫(LIB檔案)。而且應用程式事實上必須至少包含一個對DLL函式的呼叫。

二、與DLL模組建立連結

應用程式匯入函式與DLL檔案中的匯出函式進行連結有兩種方式:隱式連結和顯式連結。所謂的隱式連結是指在應用程式中不需指明DLL檔案的實際路徑,程式設計師不需關心DLL檔案的實際裝載。而顯式連結與此相反。
採用隱式連結方式,程式設計師在建立一個DLL檔案時,連結程式會自動生成一個與之對應的LIB匯入檔案。該檔案包含了每一個DLL匯出函式的符號名和可選的標識號,但是並不含有實際的程式碼。LIB檔案作為DLL的替代檔案被編譯到應用程式專案中。當程式設計師透過靜態連結方式編譯生成應用程式時,應用程式中的呼叫函式與LIB檔案中匯出符號相匹配,這些符號或標識號進入到生成的EXE檔案中。LIB檔案中也包含了對應的DLL檔名(但不是完全的路徑名),連結程式將其儲存在EXE檔案內部。當應用程式執行過程中需要載入DLL檔案時,Windows根據這些資訊發現並載入DLL,然後透過符號名或標識號實現對DLL函式的動態連結。
顯式連結方式對於整合化的開發語言(例如VB)比較適合。有了顯式連結,程式設計師就不必再使用匯入檔案,而是直接呼叫Win32 的LoadLibary函式,並指定DLL的路徑作為引數。LoadLibary返回HINSTANCE引數,應用程式在呼叫GetProcAddress函式時使用這一引數。GetProcAddress函式將符號名或標識號轉換為DLL內部的地址。假設有一個匯出如下函式的DLL檔案:
extern "C" __declspec(dllexport) double Square(double d);
下面是應用程式對該匯出函式的顯式連結的例子:
typedef double(SQRTPROC)(double);
HINSTANCE hInstance;
SQRTPROC* pFunction;
VERIFY(hInstance=::LoadLibrary("c:winntsystem32mydll.dll"));
VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
double d=(*pFunction)(81.0);//呼叫該DLL函式
在隱式連結方式中,所有被應用程式呼叫的DLL檔案都會在應用程式EXE檔案載入時被載入在到記憶體中;但如果採用顯式連結方式,程式設計師可以決定DLL檔案何時載入或不載入。顯式連結在執行時決定載入哪個DLL檔案。例如,可以將一個帶有字串資源的DLL模組以英語載入,而另一個以西班牙語載入。應用程式在選擇了合適的語種後再載入與之對應的DLL檔案。

三、使用符號名連結與標識號連結

在Win16環境中,符號名連結較低,所有那時標識號連結是主要的連結方式。在Win32環境中,符號名連結的效率得到了改善。Microsoft現在推薦使用符號名連結。但在MFC庫中的DLL版本仍然採用的是標識號連結。一個典型的MFC程式可能會連結到數百個MFC DLL函式上。採用標識號連結的應用程式的EXE檔案體相對較小,因為它不必包含匯入函式的長字串符號名。

四、編寫DllMain函式

DllMain函式是DLL模組的預設入口點。當Windows載入DLL模組時呼叫這一函式。系統首先呼叫全域性的建構函式,然後呼叫全域性函式DLLMain。DLLMain函式不僅在將DLL連結載入到程式時被呼叫,在DLL模組與程式分離時(以及其它時候)也被呼叫。下面是一個DLLMain函式的例子。
HINSTANCE g_hInstance;
extern "C" int ENTRY DllMain(HINSTANCE hInstance,D dwReason,LPVOID lpReserved)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
TRACE0("EX22A.DLL Initializing!n");
//在這裡進行初始化
}
else if(dwReason=DLL_PROCESS_DETACH)
{
TRACE0("EX22A.DLL Tenating!n");
//在這裡進行清除工作
}
return 1;//成功
}
如果程式設計師沒有為DLL模組編寫一個DLLMain函式,系統會從其它執行庫中引入一個不做任何操作的預設DLLMain函式版本。在單個執行緒啟動和終止時,DLLMain函式也被呼叫。正如由dwReason引數所表明的那樣。

五、模組控制程式碼

程式中的每個DLL模組被全域性唯一的32位元組的HINSTANCE控制程式碼標識。程式自己還有一個HINSTANCE控制程式碼。所有這些模組控制程式碼都只有在特定的程式內部有效,它們代表了DLL或EXE模組在程式虛擬空間中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,這個兩種型別可以替換使用。程式模組控制程式碼幾乎總是等於0x400000,而DLL模組的載入地址的預設控制程式碼是0x10000000。如果程式同時使用了幾個DLL模組,每一個都會有不同的HINSTANCE值。這是因為在建立DLL檔案時指定了不同的基地址,或者是因為載入程式對DLL程式碼進行了重定位。
模組控制程式碼對於載入資源特別重要。Win32 的FindRe函式中帶有一個HINSTANCE引數。EXE和DLL都有其自己的資源。如果應用程式需要來自於DLL的資源,就將此引數指定為DLL的模組控制程式碼。如果需要EXE檔案中包含的資源,就指定EXE的模組控制程式碼。
但是在使用這些控制程式碼之前存在一個問題,你怎樣得到它們呢?如果需要得到EXE模組控制程式碼,呼叫帶有Null引數的Win32函式GetModuleHandle;如果需要DLL模組控制程式碼,就呼叫以DLL檔名為引數的Win32函式GetModuleHandle。

六、應用程式怎樣找到DLL檔案

如果應用程式使用LoadLibrary顯式連結,那麼在這個函式的引數中可以指定DLL檔案的完整路徑。如果不指定路徑,或是進行隱式連結,Windows將遵循下面的搜尋順序來定位DLL:
1. 包含EXE檔案的目錄,
2. 程式的當前工作目錄,
3. Windows系統目錄,
4. Windows目錄,
5. 列在Path環境變數中的一系列目錄。
這裡有一個很容易發生錯誤的陷阱。如果你使用VC++進行專案開發,並且為DLL模組專門建立了一個專案,然後將生成的DLL檔案複製到系統目錄下,從應用程式中呼叫DLL模組。到目前為止,一切正常。接下來對DLL模組做了一些修改後重新生成了新的DLL檔案,但你忘記將新的DLL檔案複製到系統目錄下。下一次當你執行應用程式時,它仍載入了老版本的DLL檔案,這可要當心!

七、DLL程式

Microsoft 的VC++是開發和測試DLL的有效工具,只需從DLL專案中執行除錯程式即可。當你第一次這樣操作時,除錯程式會向你詢問EXE檔案的路徑。此後每次在除錯程式中執行DLL時,除錯程式會自動載入該EXE檔案。然後該EXE檔案用上面的搜尋序列發現DLL檔案,這意味著你必須設定Path環境變數讓其包含DLL檔案的磁碟路徑,或者也可以將DLL檔案複製到搜尋序列中的目錄路徑下。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-988606/,如需轉載,請註明出處,否則將追究法律責任。

相關文章