參考出處:http://www.cnblogs.com/flyingfish/archive/2007/03/28/691775.html天極網深入淺出Visual C++動態連結程式庫(Dll)編程VC++動態連結程式庫編程之讀者反饋與回覆
本文出自:http://blog.sina.com.cn/u/56cacf83010005an
比較大的應用程式都由很多模組組成,這些模組分別完成相對獨立的功能,它們彼此協作來完成整個軟體系統的工作。可能存在一些模組的功能較為通用,在構造其它軟體系統時仍會被使用。在構造軟體系統時,如果將所有模組的原始碼都靜態編譯到整個應用程式 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 檔案中也包含了對應的 DL L檔案名稱(但不是完全的路徑名),連結程式將其儲存在 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:
- 包含EXE檔案的目錄
- 進程的當前工作目錄
- Windows系統目錄
- Windows目錄
- 列在 Path 環境變數中的一系列目錄
MFC中的DLL
- Non-MFC DLL:指的是不用 MFC 的類庫結構,直接用 C 語言寫的 DLL,其輸出的函數一般用的是標準 C 介面,並能被 非 MFC 或 MFC 編寫的應用程式所調用。
- 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 所提供,不用自己顯式的寫出來。
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 類型的對象或者資源,則需要提供一個初始化函數來建立一個 CDynLinkLibrary 對象。並且,有必要把初始化函數輸出;
5、使用擴充 DLL 的 MFC 應用程式必須有一個從 CWinApp 派生的類,而且,一般在InitInstance 裡調用擴充 DLL 的初始化函數。
DLL入口函數
1、每一個 DLL 必須有一個進入點,DLLMain 是一個預設的入口函數。DLLMain 負責初始化和結束工作,每當一個新的進程或者該進程的新的線程訪問 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)。
在程式裡加入先行編譯指令,或在開發環境的項目設定裡也可以達到設定資料區段屬性的目的.必須給這些變數賦初值,否則編譯器會把沒有賦初始值的變數放在一個叫未被初始化的資料區段中。