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

來源:互聯網
上載者:User

DLL(Dynamic Link Libraries)專題:

    比較大的應用程式都由很多模組組成,這些模組分別完成相對獨立的功能,它們彼此協作來完成整個軟體系統的工作。可能存在一些模組的功能較為通用,在構造其它軟體系統時仍會被使用。在構造軟體系統時,如果將所有模組的原始碼都靜態編譯到整個應用程式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)。

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.