DLL(Dynamic Linkable Library)

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

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

    一般來說,DLL是一種磁盤文件,以.DLL、.DRV、.FON、.SYS和許多以.EXE爲擴展名的系統文件都可以是DLL。它由全局數據、服務函數和資源組成,在運行時被系統加載到進程的虛擬空間中,成爲調用進程的一部分。如果與其它DLL之間沒有衝突,該文件通常映射到進程虛擬空間的同一地址上。DLL模塊中包含各種導出函數,用於向外界提供服務。DLL可以有自己的數據段,但沒有自己的堆棧,使用與調用它的應用程序相同的堆棧模式;一個DLL在內存中只有一個實例;DLL實現了代碼封裝性;DLL的編制與具體的編程語言及編譯器無關。

    在Win32環境中,每個進程都複製了自己的讀/寫全局變量。如果想要與其它進程共享內存,必須使用內存映射文件或者聲明一個共享數據段。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、動態調用方式:是由編程者用API函數加載和卸載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類型的對象或者資源(Resources),則需要提供一個初始化函數來創建一個CDynLinkLibrary對象。並且,有必要把初始化函數輸出;
5、使用擴展DLL的MFC應用程序必須有一個從CWinApp派生的類,而且,一般在InitInstance裏調用擴展DLL的初始化函數。

三、dll入口函數:

1、每一個DLL必須有一個入口點,DllMain是一個缺省的入口函數。DllMain負責初始化(Initialization)和結束(Termination)工作,每當一個新的進程或者該進程的新的線程訪問DLL時,或者訪問DLL的每一個進程或者線程不再使用DLL或者結束時,都會調用DllMain。但是,使用TerminateProcess或TerminateThread結束進程或者線程,不會調用DllMain。

DllMain的函數原型:
BOOL APIENTRY DllMain(HANDLE hModule,DWORD 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調用約定在輸出函數名前加上一個下劃線前綴,後面加上一個“@”符號和其參數的字節數,格式爲_functionname@number

 __cdecl調用約定僅在輸出函數名前加上一個下劃線前綴,格式爲_functionname。
  
 __fastcall調用約定在輸出函數名前加上一個“@”符號,後面也是一個“@”符號和其參數的字節數,格式爲@functionname@number。

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

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

__stdcall調用約定:
          1、以“?”標識函數名的開始,後跟函數名;
          2、函數名後面以“@@YG”標識參數表的開始,後跟參數表;
          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”標識整個名字的結束,如果該函數無參數,則以“Z”標識結束。

    其格式爲“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
          int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z
          void Test2()                       -----“?Test2@@YGXXZ

__cdecl調用約定:
 規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@YG”變爲“@@YA”。

__fastcall調用約定:
 規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@YG”變爲“@@YI”。

    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)。

    在程序里加入預編譯指令,或在開發環境的項目設置裏也可以達到設置數據段屬性的目的.必須給這些變量賦初值,否則編譯器會把沒有賦初始值的變量放在一個叫未被初始化的數據段中 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章