1、建立DLL動態庫
動態鏈接庫(DLL)是從C語言函數庫和Pascal庫單元的概念發展而來的。所有的C語言標准庫函數都存放在某一函數庫中。在鏈接應用程序的過程中,鏈接器從庫文件中拷貝程序調用的函數代碼,並把這些函數代碼添加到可執行文件中。這種方法同只把函數儲存在已編譯的OBJ文件中相比更有利於代碼的重用。但隨著Windows這樣的多任務環境的出現,函數庫的方法顯得過於累贅。如果為了完成屏幕輸出、消息處理、內存管理、對話框等操作,每個程序都不得不擁有自己的函數,那麼Windows程序將變得非常龐大。Windows的發展要求允許同時運行的幾個程序共享一組函數的單一拷貝。動態鏈接庫就是在這種情況下出現的。動態鏈接庫不用重復編譯或鏈接,一旦裝入內存,DLL函數可以被系統中的任何正在運行的應用程序軟件所使用,而不必再將DLL函數的另一拷貝裝入內存。
下面我們一步一步來建立一個DLL。
一、建立一個DLL工程
新建一個工程,選擇Win32 控制台項目(Win32 Console Application),並且在應用程序設置標簽(the advanced tab)上,選擇DLL和空項目選項。
二、聲明導出函數
這裡有兩種方法聲明導出函數:一種是通過使用__declspec(dllexport),添加到需要導出的函數前,進行聲明;另外一種就是通過模塊定義文件(Module-Definition File即.DEF)來進行聲明。
第一種方法,建立頭文件DLLSample.h,在頭文件中,對需要導出的函數進行聲明。
1 #ifndef _DLL_SAMPLE_H 2 #define _DLL_SAMPLE_H 3 4 // 如果定義了C++編譯器,那麼聲明為C鏈接方式 5 #ifdef __cplusplus 6 extern "C" { 7 #endif 8 9 // 通過宏來控制是導入還是導出 10 #ifdef _DLL_SAMPLE 11 #define DLL_SAMPLE_API __declspec(dllexport) 12 #else 13 #define DLL_SAMPLE_API __declspec(dllimport) 14 #endif 15 16 // 導出/導入函數聲明 17 DLL_SAMPLE_API void TestDLL(int); 18 19 #undef DLL_SAMPLE_API 20 21 #ifdef __cplusplus 22 } 23 #endif 24 25 #endif
這個頭文件會分別被DLL和調用DLL的應用程序引入,當被DLL引入時,在DLL中定義_DLL_SAMPLE宏,這樣就會在DLL模塊中聲明函數為導出函數;當被調用DLL的應用程序引入時,就沒有定義_DLL_SAMPLE,這樣就會聲明頭文件中的函數為從DLL中的導入函數。
第二種方法:模塊定義文件是一個有著.def文件擴展名的文本文件。它被用於導出一個DLL的函數,和__declspec(dllexport)很相似,但是.def文件並不是Microsoft定義的。一個.def文件中只有兩個必需的部分:LIBRARY 和 EXPORTS。
1 LIBRARY DLLSample 2 DESCRIPTION "my simple DLL" 3 EXPORTS 4 TestDLL @1 ;@1表示這是第一個導出函數
第一行,''LIBRARY''是一個必需的部分。它告訴鏈接器(linker)如何命名你的DLL。下面被標識為''DESCRIPTION''的部分並不是必需的。該語句將字符串寫入 .rdata 節,它告訴人們誰可能使用這個DLL,這個DLL做什麼或它為了什麼(存在)。再下面的部分標識為''EXPORTS''是另一個必需的部分;這個部分使得該函數可以被其它應用程序訪問到並且它創建一個導入庫。當你生成這個項目時,不僅是一個.dll文件被創建,而且一個文件擴展名為.lib的導出庫也被創建了。除了前面的部分以外,這裡還有其它四個部分標識為:NAME, STACKSIZE, SECTIONS, 和 VERSION。另外,一個分號(;)開始一個注解,如同''//''在C++中一樣。定義了這個文件之後,頭文件中的__declspec(dllexport)就不需要聲明了。
三、編寫DllMain函數和導出函數
DllMain函數是DLL模塊的默認入口點。當Windows加載DLL模塊時調用這一函數。系統首先調用全局對象的構造函數,然後調用全局函數DLLMain。DLLMain函數不僅在將DLL鏈接加載到進程時被調用,在DLL模塊與進程分離時(以及其它時候)也被調用。
1 #include "stdafx.h" 2 #define _DLL_SAMPLE 3 4 #ifndef _DLL_SAMPLE_H 5 #include "DLLSample.h" 6 #endif 7 8 #include "stdio.h" 9 10 //APIENTRY聲明DLL函數入口點 11 BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 12 { 13 switch (ul_reason_for_call) 14 { 15 case DLL_PROCESS_ATTACH: 16 case DLL_THREAD_ATTACH: 17 case DLL_THREAD_DETACH: 18 case DLL_PROCESS_DETACH: 19 break; 20 } 21 return TRUE; 22 } 23 24 void TestDLL(int arg) 25 { 26 printf("DLL output arg %d\n", arg); 27 }
如果程序員沒有為DLL模塊編寫一個DLLMain函數,系統會從其它運行庫中引入一個不做任何操作的缺省DLLMain函數版本。在單個線程啟動和終止時,DLLMain函數也被調用。 然後,F7編譯,就得到一個DLL了。
上文簡單的介紹了如何建立一個簡單DLL,下面再我簡單的介紹一下如何使用一個DLL。當一個DLL被生成後,它創建了一個.dll文件和一個.lib文件;這兩個都是你需要的。要使用DLL,就需要載入這個DLL。
隱式鏈接
這裡有兩個方法來載入一個DLL;一個方法是捷徑另一個則相比要復雜些。捷徑是只鏈接到你.lib 文件並將.dll文件置入你的新項目的路徑中去。因此,創建一個新的空的Win32控制台項目並添加一個源文件。將你做的DLL放入你的新項目相同的目錄下。
1 #include "stdafx.h" 2 #include "DLLSample.h" 3 4 #pragma comment(lib, "DLLSample.lib") //你也可以在項目屬性中設置庫的鏈接 5 6 int main() 7 { 8 TestDLL(123); 9 return(1); 10 }
這就是載入一個DLL的簡單方法。
顯式鏈接
難點的加載DLL的方法稍微有點復雜。你將需要函數指針和一些Windows函數。但是,通過這種載入DLLs的方法,你不需要DLL的.lib或頭文件,而只需要DLL。
1 #include <iostream> 2 #include <windows.h> 3 typedef void (*DLLFunc)(int); 4 int main() 5 { 6 DLLFunc dllFunc; 7 HINSTANCE hInstLibrary = LoadLibrary("DLLSample.dll"); 8 9 if (hInstLibrary == NULL) 10 { 11 FreeLibrary(hInstLibrary); 12 } 13 dllFunc = (DLLFunc)GetProcAddress(hInstLibrary, "TestDLL"); 14 if (dllFunc == NULL) 15 { 16 FreeLibrary(hInstLibrary); 17 } 18 dllFunc(123); 19 std::cin.get(); 20 FreeLibrary(hInstLibrary); 21 return(1); 22 }
首先你會注意到:這裡包括進了文件“windows.h”同時移走了“DLLSample.h”。原因很簡單:因為windows.h包含了一些Windows函數,當然你現在將只需要其中幾個而已。它也包含了一些將會用到的Windows特定變量。你可以去掉DLL的頭文件(DLLSample.h)因為-如我前面所說-當你使用這個方法載入DLL時你並不需要它。
下面你會看到:下面的一句代碼:
typedef void (*DLLFunc)(int);
這是一個函數指針類型的定義。指向一個函數是一個int型的參數,返回值為void類型。一個HINSTANCE是一個Windows數據類型:是一個實例的句柄;在此情況下,這個實例將是這個DLL。你可以通過使用函數LoadLibrary()獲得DLL的實例,它獲得一個名稱作為參數。在調用LoadLibrary函數後,你必需查看一下函數返回是否成功。你可以通過檢查HINSTANCE是否等於NULL(在Windows.h中定義為0或Windows.h包含的一個頭文件)來查看其是否成功。如果其等於NULL,該句柄將是無效的,並且你必需釋放這個庫。換句話說,你必需釋放DLL獲得的內存。如果函數返回成功,你的HINSTANCE就包含了指向DLL的句柄。
一旦你獲得了指向DLL的句柄,你現在可以從DLL中重新獲得函數。為了這樣作,你必須使用函數GetProcAddress(),它將DLL的句柄(你可以使用HINSTANCE)和函數的名稱作為參數。你可以讓函數指針獲得由GetProcAddress()返回的值,同時你必需將GetProcAddress()轉換為那個函數定義的函數指針。舉個例子,對於Add()函數,你必需將GetProcAddress()轉換為AddFunc;這就是它知道參數及返回值的原因。現在,最好先確定函數指針是否等於NULL以及它們擁有DLL的函數。這只是一個簡單的if語句;如果其中一個等於NULL,你必需如前所述釋放庫。
一旦函數指針擁有DLL的函數,你現在就可以使用它們了,但是這裡有一個需要注意的地方:你不能使用函數的實際名稱;你必需使用函數指針來調用它們。在那以後,所有你需要做的是釋放庫如此而已。
模塊句柄
進程中的每個DLL模塊被全局唯一的32字節的HINSTANCE句柄標識。進程自己還有一個HINSTANCE句柄。所有這些模塊句柄都只有在特定的進程內部有效,它們代表了DLL或EXE模塊在進程虛擬空間中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,這個兩種類型可以替換使用。進程模塊句柄幾乎總是等於0x400000,而DLL模塊的加載地址的缺省句柄是0x10000000。如果程序同時使用了幾個DLL模塊,每一個都會有不同的HINSTANCE值。這是因為在創建DLL文件時指定了不同的基地址,或者是因為加載程序對DLL代碼進行了重定位。
模塊句柄對於加載資源特別重要。Win32 的FindResource函數中帶有一個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文件拷貝到搜索序列中的目錄路徑下。
或者當你調試EXE程序時,在Project Setting中,將Debug選項卡中的Category設置為Additional DLLs。就可以同時調試EXE和它調用的DLL(當然,你需要有DLL的源代碼)了。
前面介紹了怎麼從DLL中導出函數,下面我們來看一下如何從DLL中導出變量來。聲明為導出變量時,同樣有兩種方法:
第一種是用__declspec進行導出聲明
1 #ifndef _DLL_SAMPLE_H 2 #define _DLL_SAMPLE_H 3 4 // 如果定義了C++編譯器,那麼聲明為C鏈接方式 5 #ifdef __cplusplus 6 extern "C" { 7 #endif 8 9 // 通過宏來控制是導入還是導出 10 #ifdef _DLL_SAMPLE 11 #define DLL_SAMPLE_API __declspec(dllexport) 12 #else 13 #define DLL_SAMPLE_API __declspec(dllimport) 14 #endif 15 16 // 導出/導入變量聲明 17 DLL_SAMPLE_API extern int DLLData; 18 19 #undef DLL_SAMPLE_API 20 21 #ifdef __cplusplus 22 } 23 #endif 24 25 #endif
第二種是用模塊定義文件(.def)進行導出聲明1 LIBRARY DLLSample 2 DESCRIPTION "my simple DLL" 3 EXPORTS 4 DLLData DATA ;DATA表示這是數據(變量)
下面是DLL的實現文件 1 #include "stdafx.h" 2 #define _DLL_SAMPLE 3 4 #ifndef _DLL_SAMPLE_H 5 #include "DLLSample.h" 6 #endif 7 8 #include "stdio.h" 9 10 int DLLData; 11 12 //APIENTRY聲明DLL函數入口點 13 BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 14 { 15 switch (ul_reason_for_call) 16 { 17 case DLL_PROCESS_ATTACH: 18 DLLData = 123; // 在入口函數中對變量進行初始化 19 break 20 case DLL_THREAD_ATTACH: 21 case DLL_THREAD_DETACH: 22 case DLL_PROCESS_DETACH: 23 break; 24 } 25 return TRUE; 26 }
同樣,應用程序調用DLL中的變量也有兩種方法。
第一種是隱式鏈接:
1 #include <stdio.h> 2 #include "DLLSample.h" 3 4 #pragma comment(lib,"DLLSample.lib") 5 6 7 int main(int argc, char *argv[]) 8 { 9 printf("%d ", DLLSample); 10 return 0; 11 }
第二種是顯式鏈接: 1 #include <iostream> 2 #include <windows.h> 3 4 int main() 5 { 6 int my_int; 7 HINSTANCE hInstLibrary = LoadLibrary("DLLSample.dll"); 8 9 if (hInstLibrary == NULL) 10 { 11 FreeLibrary(hInstLibrary); 12 } 13 my_int = *(int*)GetProcAddress(hInstLibrary, "DLLData"); 14 if (dllFunc == NULL) 15 { 16 FreeLibrary(hInstLibrary); 17 } 18 std::cout<<my_int; 19 std::cin.get(); 20 FreeLibrary(hInstLibrary); 21 return(1); 22 }
通過GetProcAddress取出的函數或者變量都是地址,因此,需要解引用並且轉類型。
前面介紹了怎麼從DLL中導出函數和變量,實際上導出類的方法也是大同小異,廢話就不多說了,下面給個簡單例子示范一下,也就不多做解釋了。
DLL頭文件:
1 #ifndef _DLL_SAMPLE_H 2 #define _DLL_SAMPLE_H 3 4 // 通過宏來控制是導入還是導出 5 #ifdef _DLL_SAMPLE 6 #define DLL_SAMPLE_API __declspec(dllexport) 7 #else 8 #define DLL_SAMPLE_API __declspec(dllimport) 9 #endif 10 11 // 導出/導入變量聲明 12 DLL_SAMPLE_API class DLLClass 13 { 14 public: 15 void Show(); 16 }; 17 18 #undef DLL_SAMPLE_API 19 20 #endif
DLL實現文件: 1 #include "stdafx.h" 2 #define _DLL_SAMPLE 3 4 #ifndef _DLL_SAMPLE_H 5 #include "DLLSample.h" 6 #endif 7 8 #include "stdio.h" 9 10 //APIENTRY聲明DLL函數入口點 11 BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 12 { 13 switch (ul_reason_for_call) 14 { 15 case DLL_PROCESS_ATTACH: 16 case DLL_THREAD_ATTACH: 17 case DLL_THREAD_DETACH: 18 case DLL_PROCESS_DETACH: 19 break; 20 } 21 return TRUE; 22 } 23 24 void DLLClass::Show() 25 { 26 printf("DLLClass show!"); 27 }
應用程序調用DLL
1 #include "DLLSample.h" 2 3 #pragma comment(lib,"DLLSample.lib") 4 5 6 int main(int argc, char *argv[]) 7 { 8 DLLClass dc; 9 dc.Show(); 10 return 0; 11 }
大家可能發現了,上面我沒有使用模塊定義文件(.def)聲明導出類也沒有用顯式鏈接導入DLL。用Depends查看前面編譯出來的DLL文件,會發現裡面導出了很奇怪的symbol,這是因為C++編譯器在編譯時會對symbol進行修飾。這是我從別人那兒轉來的截圖。
網上找了下,發現了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++調用。
雖然因為C++編譯器對symbol進行修飾的原因不能直接用def文件聲明導出類和顯式鏈接,但是可以用另外一種取巧的方式。
在頭文件中類的聲明中添加一個友元函數:
friend DLLClass* CreatDLLClass();
然後聲明CreatDLLClass()為導出函數,通過調用該函數返回一個DLLClass類的對象,同樣達到了導出類的目的。
這樣,就可以用顯式鏈接來調用CreatDLLClass(),從而得到類對象了。
在Win16環境中,DLL的全局數據對每個載入它的進程來說都是相同的,因為所有的進程用的都收同一塊地址空間;而在Win32環境中,情況卻發生了變化,每個進程都有了它自己的地址空間,DLL函數中的代碼所創建的任何對象(包括變量)都歸調用它的進程所有。當進程在載入DLL時,操作系統自動把DLL地址映射到該進程的私有空間,也就是進程的虛擬地址空間,而且也復制該DLL的全局數據的一份拷貝到該進程空間。(在物理內存中,多進程載入DLL時,DLL的代碼段實際上是只加載了一次,只是將物理地址映射到了各個調用它的進程的虛擬地址空間中,而全局數據會在每個進程都分別加載)。也就是說每個進程所擁有的相同的DLL的全局數據,它們的名稱相同,但其值卻並不一定是相同的,而且是互不干涉的。
因此,在Win32環境下要想在多個進程中共享數據,就必須進行必要的設置。在訪問同一個Dll的各進程之間共享存儲器是通過存儲器映射文件技術實現的。也可以把這些需要共享的數據分離出來,放置在一個獨立的數據段裡,並把該段的屬性設置為共享。必須給這些變量賦初值,否則編譯器會把沒有賦初始值的變量放在一個叫未被初始化的數據段中。
在DLL的實現文件中添加下列代碼:
1 #pragma data_seg("DLLSharedSection") // 聲明共享數據段,並命名該數據段 2 int SharedData = 123; // 必須在定義的同時進行初始化!!!! 3 #pragma data_seg()
在#pragma data_seg("DLLSharedSection")和#pragma data_seg()之間的所有變量將被訪問該Dll的所有進程看到和共享。僅定義一個數據段還不能達到共享數據的目的,還要告訴編譯器該段的屬性,有三種方法可以實現該目的(其效果是相同的),一種方法是在.DEF文件中加入如下語句:1 SETCTIONS 2 DLLSharedSection READ WRITE SHARED
另一種方法是在項目設置的鏈接選項(Project Setting --〉Link)中加入如下語句:
1 /SECTION:DLLSharedSection,rws
還有一種就是使用指令:
1 #pragma comment(linker,"/section:.DLLSharedSection,rws")
那麼這個數據節中的數據可以在所有DLL的實例之間共享了。所有對這些數據的操作都針對同一個實例的,而不是在每個進程的地址空間中都有一份。
當進程隱式或顯式調用一個動態庫裡的函數時,系統都要把這個動態庫映射到這個進程的虛擬地址空間裡。這使得DLL成為進程的一部分,以這個進程的身份執行,使用這個進程的堆棧。
下面來談一下在具體使用共享數據段時需要注意的一些問題:
· 所有在共享數據段中的變量,只有在數據段中經過了初始化之後,才會是進程間共享的。如果沒有初始化,那麼進程間訪問該變量則是未定義的。
· 所有的共享變量都要放置在共享數據段中。如何定義很大的數組,那麼也會導致很大的DLL。
· 不要在共享數據段中存放進程相關的信息。Win32中大多數的數據結構和值(比如HANDLE)只在特定的進程上下文中才是有效地。
· 每個進程都有它自己的地址空間。因此不要在共享數據段中共享指針,指針指向的地址在不同的地址空間中是不一樣的。
· DLL在每個進程中是被映射在不同的虛擬地址空間中的,因此函數指針也是不安全的。
當然還有其它的方法來進行進程間的數據共享,比如文件內存映射等,這就涉及到通用的進程間通信了,這裡就不多講了。
轉自:http://www.cppblog.com/suiaiguo/archive/2009/07/20/90619.html