我的Dll(動態連結庫)學習筆記 (轉)

worldblog發表於2007-12-12
我的Dll(動態連結庫)學習筆記 (轉)[@more@]

DLL(Dynamic Link Libraries)專題:

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

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

  一般來說,DLL是一種磁碟檔案,以.DLL、.DRV、.FON、.SYS和許多以.EXE為副檔名的系統檔案都可以是DLL。它由全域性資料、服務和資源組成,在執行時被系統載入到程式的虛擬空間中,成為呼叫程式的一部分。如果與其它DLL之間沒有衝突,該檔案通常對映到程式虛擬空間的同一地址上。DLL模組中包含各種匯出函式,用於向外界提供服務。DLL可以有自己的資料段,但沒有自己的堆疊,使用與呼叫它的應用程式相同的堆疊;一個DLL在記憶體中只有一個例項;DLL實現了程式碼封裝性;DLL的編制與具體的程式語言及無關。

  在環境中,每個程式都複製了自己的讀/寫全域性變數。如果想要與其它程式共享記憶體,必須使用記憶體對映檔案或者宣告一個共享資料段。DLL模組需要的堆疊記憶體都是從執行程式的堆疊中分配出來的。Windows在載入DLL模組時將程式函式呼叫與DLL檔案的匯出函式相匹配。Windows對DLL的操作僅僅是把DLL對映到需要它的程式的虛擬地址空間裡去。DLL函式中的程式碼所建立的任何(包括變數)都歸呼叫它的執行緒或程式所有. 

一、關於呼叫方式:

1、靜態呼叫方式:由編譯系統完成對DLL的載入和應用程式結束時DLL解除安裝的編碼(如還有其它程式使用該DLL,則Windows對DLL的應用記錄減1,直到所有相關程式都結束對該DLL的使用時才釋放它),簡單實用,但不夠靈活,只能滿足一般要求。

 隱式的呼叫:需要把產生動態連線庫時產生的.LIB檔案加入到應用程式的工程中,想使用DLL中的函式時,只須說明一下。隱式呼叫不需要呼叫LoadLibrary()和FreeLibrary()。程式設計師在建立一個DLL檔案時,連結程式會自動生成一個與之對應的LIB匯入檔案。該檔案包含了每一個DLL匯出函式的符號名和可選的標識號,但是並不含有實際的程式碼。LIB檔案作為DLL的替代檔案被編譯到應用程式專案中。當程式設計師透過靜態連結方式編譯生成應用程式時,應用程式中的呼叫函式與LIB檔案中匯出符號相匹配,這些符號或標識號進入到生成的EXE檔案中。LIB檔案中也包含了對應的DLL檔名(但不是完全的路徑名),連結程式將其在EXE檔案內部。當應用程式執行過程中需要載入DLL檔案時,Windows根據這些資訊發現並載入DLL,然後透過符號名或標識號實現對DLL函式的動態連結。所有被應用程式呼叫的DLL檔案都會在應用程式EXE檔案載入時被載入在到記憶體中。可程式連結到一個包含DLL輸出函式資訊的輸入庫檔案(.LIB檔案)。作業系統在載入使用可執行程式時載入DLL。可執行程式直接透過函式名呼叫DLL的輸出函式,呼叫方法和程式內部其他的函式是一樣的。


2、動態呼叫方式:是由程式設計者用函式載入和解除安裝DLL來達到呼叫DLL的目的,使用上較複雜,但能更加有效地使用記憶體,是編制大型應用程式時的重要方式。

 顯式的呼叫:是指在應用程式中用LoadLibrary或MFC提供的AfxLoadLibrary顯式的將自己所做的動態連線庫調進來,動態連線庫的檔名即是上面兩個函式的引數,再用GetProcAddress()獲取想要引入的函式。自此,你就可以象使用如同本應用程式自定義的函式一樣來呼叫此引入函式了。在應用程式退出之前,應該用FreeLibrary或MFC提供的AfxFreeLibrary釋放動態連線庫。直接呼叫Win32 的LoadLibary函式,並指定DLL的路徑作為引數。LoadLibary返回HINSTANCE引數,應用程式在呼叫GetProcAddress函式時使用這一引數。GetProcAddress函式將符號名或標識號轉換為DLL內部的地址。程式設計師可以決定DLL檔案何時載入或不載入,顯式連結在執行時決定載入哪個DLL檔案。使用DLL的程式在使用之前必須載入(LoadLibrary)載入DLL從而得到一個DLL模組的控制程式碼,然後呼叫GetProcAddress函式得到輸出函式的指標,在退出之前必須解除安裝DLL(FreeLibrary)。

  Windows將遵循下面的搜尋順序來定位DLL:
1.包含EXE檔案的目錄,
2.程式的當前工作目錄,
3.Windows系統目錄,
4.Windows目錄,
5.列在Path環境變數中的一系列目錄。

二、MFC中的dll:

a、Non-MFC DLL:指的是不用MFC的類庫結構,直接用C語言寫的DLL,其輸出的函式一般用的是標準C介面,並能被非MFC或MFC編寫的應用程式所呼叫。

b、Regular DLL:和下述的Extension Dlls一樣,是用MFC類庫編寫的。明顯的特點是在原始檔裡有一個繼承CWinApp的類。其又可細分成靜態連線到MFC和動態連線到MFC上的。

靜態連線到MFC的動態連線庫只被VC的專業般和企業版所支援。該類DLL應用程式裡頭的輸出函式可以被任意Win32程式使用,包括使用MFC的應用程式。輸入函式有如下形式:
extern "C" EXPORT YourExportedFunction( );
如果沒有extern “C”修飾,輸出函式僅僅能從C++程式碼中呼叫。
DLL應用程式從CWinApp派生,但沒有訊息迴圈。

動態連結到MFC的規則DLL應用程式裡頭的輸出函式可以被任意Win32程式使用,包括使用MFC的應用程式。但是,所有從DLL輸出的函式應該以如下語句開始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ))
此語句用來正確地切換MFC模組狀態。

Regular DLL能夠被所有支援DLL技術的語言所編寫的應用程式所呼叫。在這種動態連線庫中,它必須有一個從CWinApp繼承下來的類,DllMain函式被MFC所提供,不用自己顯式的寫出來。

c、Extension DLL:用來實現從MFC所繼承下來的類的重新利用,也就是說,用這種型別的動態連線庫,可以用來輸出一個從MFC所繼承下來的類。它輸出的函式僅可以被使用MFC且動態連結到MFC的應用程式使用。可以從MFC繼承你所想要的、更適於你自己用的類,並把它提供給你的應用程式。你也可隨意的給你的應用程式提供MFC或MFC繼承類的物件指標。Extension DLL使用MFC的動態連線版本所建立的,並且它只被用MFC類庫所編寫的應用程式所呼叫。Extension DLLs 和Regular DLLs不一樣,它沒有一個從CWinApp繼承而來的類的物件,所以,你必須為自己DllMain函式新增初始化程式碼和結束程式碼。

和規則DLL相比,有以下不同:

1、它沒有一個從CWinApp派生的物件;
2、它必須有一個DllMain函式;
3、DllMain呼叫AfxInitExtensionModule函式,必須檢查該函式的返回值,如果返回0,DllMmain也返回0;
4、如果它希望輸出CRuntimeClass型別的物件或者資源(Res),則需要提供一個初始化函式來建立一個CDynLinkLibrary物件。並且,有必要把初始化函式輸出;
5、使用擴充套件DLL的MFC應用程式必須有一個從CWinApp派生的類,而且,一般在InitInstance裡呼叫擴充套件DLL的初始化函式。

三、dll入口函式:

1、每一個DLL必須有一個入口點,DllMain是一個預設的入口函式。DllMain負責初始化(Initialization)和結束(Tenation)工作,每當一個新的程式或者該程式的新的執行緒訪問DLL時,或者訪問DLL的每一個程式或者執行緒不再使用DLL或者結束時,都會呼叫DllMain。但是,使用TerminateProcess或TerminateThread結束程式或者執行緒,不會呼叫DllMain。

DllMain的函式原型:
BOOL APIENTRY DllMain(HANDLE hModule,D ul_reason_for_call,LPVOID lpReserved)
{
 switch(ul_reason_for_call)
 {
 case DLL_PROCESS_ATTACH:
 .......
 case DLL_THREAD_ATTACH:
 .......
 case DLL_THREAD_DETACH:
 .......
 case DLL_PROCESS_DETACH:
 .......
 return TRUE;
 }
}

引數:
hMoudle:是動態庫被呼叫時所傳遞來的一個指向自己的控制程式碼(實際上,它是指向_DGROUP段的一個選擇符);
ul_reason_for_call:是一個說明動態庫被調原因的標誌。當程式或執行緒裝入或解除安裝動態連線庫的時候,作業系統呼叫入口函式,並說明動態連線庫被呼叫的原因。它所有的可能值為:
DLL_PROCESS_ATTACH: 程式被呼叫;
DLL_THREAD_ATTACH: 執行緒被呼叫;
DLL_PROCESS_DETACH: 程式被停止;
DLL_THREAD_DETACH: 執行緒被停止;
lpReserved:是一個被系統所保留的引數。

2、_DllMainCRTStartup

 為了使用“C”執行庫(CRT,C Run time Library)的DLL版本(多執行緒),一個DLL應用程式必須指定_DllMainCRTStartup為入口函式,DLL的初始化函式必須是DllMain。

 _DllMainCRTStartup完成以下任務:當程式或執行緒捆綁(Attach)到DLL時為“C”執行時的資料(C Runtime Data)分配空間和初始化並且構造全域性“C++”物件,當程式或者執行緒終止使用DLL(Detach)時,清理C Runtime Data並且銷燬全域性“C++”物件。它還呼叫DllMain和RawDllMain函式。

 RawDllMain在DLL應用程式動態連結到MFC DLL時被需要,但它是靜態的連結到DLL應用程式的。在講述狀態管理時解釋其原因。

四、關於約定:

動態庫輸出函式的約定有兩種:呼叫約定和名字修飾約定。

1)呼叫約定(Calling convention):決定函式引數傳送時入棧和出棧的順序,由呼叫者還是被呼叫者把引數彈出棧,以及編譯器用來識別函式名字的修飾約定。

函式呼叫約定有多種,這裡簡單說一下:

  1、__stdcall呼叫約定相當於16位動態庫中經常使用的PASCAL呼叫約定。在32位的VC++5.0中PASCAL呼叫約定不再被支援(實際上它已被定義為__stdcall。除了__pascal外,__fortran和__syscall也不被支援),取而代之的是__stdcall呼叫約定。兩者實質上是一致的,即函式的引數自右向左透過棧傳遞,被呼叫的函式在返回前清理傳送引數的記憶體棧,但不同的是函式名的修飾部分(關於函式名的修飾部分在後面將詳細說明)。

  _stdcall是Pascal程式的預設呼叫方式,通常用於Win32 Api中,函式採用從右到左的壓棧方式,自己在退出時清空堆疊。VC將函式編譯後會在函式名前面加上下劃線字首,在函式名後加上"@"和引數的位元組數。

  2、C呼叫約定(即用__cdecl關鍵字說明)按從右至左的順序壓引數入棧,由呼叫者把引數彈出棧。對於傳送引數的記憶體棧是由呼叫者來維護的(正因為如此,實現可變引數的函式只能使用該呼叫約定)。另外,在函式名修飾約定方面也有所不同。

  _cdecl是C和C++程式的預設呼叫方式。每一個呼叫它的函式都包含清空堆疊的程式碼,所以產生的可執行檔案大小會比呼叫_stdcall函式的大。函式採用從右到左的壓棧方式。VC將函式編譯後會在函式名前面加上下劃線字首。是MFC預設呼叫約定。

  3、__fastcall呼叫約定是“人”如其名,它的主要特點就是快,因為它是透過暫存器來傳送引數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的引數,剩下的引數仍舊自右向左壓棧傳送,被呼叫的函式在返回前清理傳送引數的記憶體棧),在函式名修飾約定方面,它和前兩者均不同。

  _fastcall方式的函式採用暫存器傳遞引數,VC將函式編譯後會在函式名前面加上"@"字首,在函式名後加上"@"和引數的位元組數。 

  4、thiscall僅僅應用於“C++”成員函式。this指標存放於CX暫存器,引數從右到左壓。thiscall不是關鍵詞,因此不能被程式設計師指定。

  5、naked call採用1-4的呼叫約定時,如果必要的話,進入函式時編譯器會產生程式碼來儲存ESI,EDI,EBX,EBP暫存器,退出函式時則產生程式碼恢復這些暫存器的內容。naked call不產生這樣的程式碼。naked call不是型別修飾符,故必須和_declspec共同使用。

  關鍵字 __stdcall、__cdecl和__fastcall可以直接加在要輸出的函式前,也可以在編譯環境的Setting...C/C++ Code Generation項選擇。當加在輸出函式前的關鍵字與編譯環境中的選擇不同時,直接加在輸出函式前的關鍵字有效。它們對應的命令列引數分別為/Gz、/Gd和/Gr。預設狀態為/Gd,即__cdecl。

  要完全模仿PASCAL呼叫約定首先必須使用__stdcall呼叫約定,至於函式名修飾約定,可以透過其它方法模仿。還有一個值得一提的是WINAPI宏,Windows.h支援該宏,它可以將出函式翻譯成適當的呼叫約定,在WIN32中,它被定義為__stdcall。使用WINAPI宏可以建立自己的APIs。

2)名字修飾約定

1、修飾名(Decoration name)

“C”或者“C++”函式在內部(編譯和連結)透過修飾名識別。修飾名是編譯器在編譯函式定義或者原型時生成的字串。有些情況下使用函式的修飾名是必要的,如在模組定義檔案裡頭指定輸出“C++”過載函式、建構函式、解構函式,又如在程式碼裡呼叫“C””或“C++”函式等。

修飾名由函式名、類名、呼叫約定、返回型別、引數等共同決定。

2、名字修飾約定隨呼叫約定和編譯種類(C或C++)的不同而變化。函式名修飾約定隨編譯種類和呼叫約定的不同而不同,下面分別說明。

  a、C編譯時函式名修飾約定規則:

 __stdcall呼叫約定在輸出函式名前加上一個下劃線字首,後面加上一個“@”符號和其引數的位元組數,格式為to:_functionname@number">_functionname@number。

 __cdecl呼叫約定僅在輸出函式名前加上一個下劃線字首,格式為_functionname。
 
 __fastcall呼叫約定在輸出函式名前加上一個“@”符號,後面也是一個“@”符號和其引數的位元組數,格式為@functionname@number。

  它們均不改變輸出函式名中的字元大小寫,這和PASCAL呼叫約定不同,PASCAL約定輸出的函式名無任何修飾且全部大寫。

  b、C++編譯時函式名修飾約定規則:

__stdcall呼叫約定:
  1、以“?”標識函式名的開始,後跟函式名;
  2、函式名後面以”標識參數列的開始,後跟參數列;
  3、參數列以代號表示:
  X--void ,
  D--char,
  E--unsigned char,
  F--short,
  H--int,
  I--unsigned int,
  J--long,
  K--unsigned long,
  M--float,
  N--double,
  _N--bool,
  ....
  PA--表示指標,後面的代號表明指標型別,如果相同型別的指標連續出現,以“0”代替,一個“0”代表一次重複;
  4、參數列的第一項為該函式的返回值型別,其後依次為引數的資料型別,指標標識在其所指資料型別前;
  5、參數列後以”標識整個名字的結束,如果該函式無引數,則以“Z”標識結束。

  其格式為”或”,例如
  int Test1(char *var1,unsigned long)”
  void Test2()  ”

__cdecl呼叫約定:
 規則同上面的_stdcall呼叫約定,只是參數列的開始標識由上面的”變為”。

__fastcall呼叫約定:
 規則同上面的_stdcall呼叫約定,只是參數列的開始標識由上面的”變為”。

  VC++對函式的省缺宣告是"__cedcl",將只能被C/C++呼叫.
 
五、關於DLL的函式:

  動態連結庫中定義有兩種函式:匯出函式(export function)和內部函式(internal function)。匯出函式可以被其它模組呼叫,內部函式在定義它們的DLL程式內部使用。

輸出函式的方法有以下幾種:

1、傳統的方法

 在模組定義檔案的EXPORT部分指定要輸入的函式或者變數。語法格式如下:
entryname[=internalname] [@ordinal[NONAME]] [DATA] [PRIVATE]

其中:

entryname是輸出的函式或者資料被引用的名稱;

internalname同entryname;

@ordinal表示在輸出表中的順序號(index);

NONAME僅僅在按順序號輸出時被使用(不使用entryname);

DATA表示輸出的是資料項,使用DLL輸出資料的程式必須宣告該資料項為_declspec(dllimport)。

上述各項中,只有entryname項是必須的,其他可以省略。

 對於“C”函式來說,entryname可以等同於函式名;但是對“C++”函式(成員函式、非成員函式)來說,entryname是修飾名。可以從.map映像檔案中得到要輸出函式的修飾名,或者使用DUMPBIN /SYMBOLS得到,然後把它們寫在.def檔案的輸出模組。DUMPBIN是VC提供的一個工具。

如果要輸出一個“C++”類,則把要輸出的資料和成員的修飾名都寫入.def模組定義檔案。

2、在命令列輸出

 對連結程式LINK指定/EXPORT命令列引數,輸出有關函式。

3、使用MFC提供的修飾符號_declspec(dllexport)

 在要輸出的函式、類、資料的宣告前加上_declspec(dllexport)的修飾符,表示輸出。__declspec(dllexport)在C呼叫約定、C編譯情況下可以去掉輸出函式名的下劃線字首。extern "C"使得在C++中使用C編譯方式成為可能。在“C++”下定義“C”函式,需要加extern “C”關鍵詞。用extern "C"來指明該函式使用C編譯方式。輸出的“C”函式可以從“C”程式碼裡呼叫。
 
  例如,在一個C++檔案中,有如下函式:
  extern "C" {void  __declspec(dllexport) __cdecl Test(int var);}
其輸出函式名為:Test
 
 MFC提供了一些宏,就有這樣的作用。

AFX_CLASS_IMPORT:__declspec(dllexport)
 
AFX_API_IMPORT:__declspec(dllexport)
 
AFX_DATA_IMPORT:__declspec(dllexport)
 
AFX_CLASS_EXPORT:__declspec(dllexport)
 
AFX_API_EXPORT:__declspec(dllexport)
 
AFX_DATA_EXPORT:__declspec(dllexport)
 
AFX_EXT_CLASS: #ifdef _AFXEXT
 AFX_CLASS_EXPORT
 #else
 AFX_CLASS_IMPORT
 
AFX_EXT_API:#ifdef _AFXEXT
  AFX_API_EXPORT
  #else
  AFX_API_IMPORT
 
AFX_EXT_DATA:#ifdef _AFXEXT
  AFX_DATA_EXPORT
  #else
  AFX_DATA_IMPORT

 像AFX_EXT_CLASS這樣的宏,如果用於DLL應用程式的實現中,則表示輸出(因為_AFX_EXT被定義,通常是在編譯器的標識引數中指定該選項/D_AFX_EXT);如果用於使用DLL的應用程式中,則表示輸入(_AFX_EXT沒有定義)。

 要輸出整個的類,對類使用_declspec(_dllexpot);要輸出類的成員函式,則對該函式使用_declspec(_dllexport)。如:

class AFX_EXT_CLASS CTextDoc : public CDocument
{
 …
}

extern "C" AFX_EXT_API void WINAPI InitMYDLL();

 這幾種方法中,最好採用第三種,方便好用;其次是第一種,如果按順序號輸出,呼叫會高些;最次是第二種。 

六、模組定義檔案(.DEF)

 模組定義檔案(.DEF)是一個或多個用於描述DLL屬性的模組語句組成的文字檔案,每個DEF檔案至少必須包含以下模組定義語句:

* 第一個語句必須是LIBRARY語句,指出DLL的名字;
* EXPORTS語句列出被匯出函式的名字;將要輸出的函式修飾名羅列在EXPORTS之下,這個名字必須與定義函式的名字完全一致,如此就得到一個沒有任何修飾的函式名了。
* 可以使用DESCRIPTION語句描述DLL的用途(此句可選);
* ";"對一行進行註釋(可選)。

七、DLL程式和呼叫其輸出函式的程式的關係

1、dll與程式、執行緒之間的關係

DLL模組被對映到呼叫它的程式的虛擬地址空間。
DLL使用的記憶體從呼叫程式的虛擬地址空間分配,只能被該程式的執行緒所訪問。
DLL的控制程式碼可以被呼叫程式使用;呼叫程式的控制程式碼可以被DLL使用。
DLL使用呼叫程式的棧。

2、關於共享資料段

 DLL定義的全域性變數可以被呼叫程式訪問;DLL可以訪問呼叫程式的全域性資料。使用同一DLL的每一個程式都有自己的DLL全域性變數例項。如果多個執行緒併發訪問同一變數,則需要使用同步機制;對一個DLL的變數,如果希望每個使用DLL的執行緒都有自己的值,則應該使用執行緒區域性儲存(TLS,Thread Local Strorage)。

  在程式里加入預編譯指令,或在開發環境的專案設定裡也可以達到設定資料段屬性的目的.必須給這些變數賦初值,否則編譯器會把沒有賦初始值的變數放在一個叫未被初始化的資料段中。

 rivershan原創於2002年9月18日


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

相關文章