轉自:http://blog.chinaunix.net/uid-20255102-id-1713076.html
!建議耐心看完!
鏈接庫分為靜態鏈接庫和動態鏈接庫,而動態鏈接庫在使用時,又進一步分為裝載時鏈接和運行時鏈接。裝載時鏈接是指該動態鏈接庫是在程序裝入時進行加載鏈接的,而運行時鏈接是指該動態鏈接庫是在程序運行時執行LoadLibrary(或LoadLibraryEx,下同)函數動態加載的。因此,由於動態鏈接庫有這兩種鏈接方式,所以在編寫使用DLL的程序時,就有了兩種可選方案。
可能有人會問“為什么需要裝載時鏈接?直接靜態鏈接不就行了嗎?”,這是模塊化程序設計的需要。試想,如果你開發一個很大的程序,並且經常需要更新。如果你選擇靜態鏈接,那么每次更新就必須更新整個exe文件,而如果你把需要經常更新的模塊做成dll,那么只需要更新這個文件即可,每次程序運行時加載這個更新的文件即可。
/* * dll_lib.h */ #ifndef DLL_LIB_H #define DLL_LIB_H #ifdef __cplusplus #define EXPORT extern "C" __declspec (dllexport) #else #define EXPORT __declspec (dllexport) #endif EXPORT int WINAPI GetMax(int a, int b); #endif
/* * dll_lib.c */ #include <windows.h> #include <stdio.h> #include "dll_lib.h" int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: printf("> process attach of dll\n"); break; case DLL_THREAD_ATTACH: printf("> thread attach of dll\n"); break; case DLL_THREAD_DETACH: printf("> thread detach of dll\n"); break; case DLL_PROCESS_DETACH: printf("> process detach of dll\n"); break; } return TRUE; } int GetMax(int a, int b) { return a > b ? a : b; }
接着,再創建一個Win32 Console Application工程DLL_Test,同樣將該工程加入先前的DLLTest工作區中,並直接保存在該工作區目錄下。然后向工程DLL_Test加入下面的文件:
/* * testMain.c */ #include <windows.h> #include <stdio.h> #include "dll_lib.h" int main() { int a = 2; int b = 3; printf(" max(2, 3) = %d\n", GetMax(2, 3)); return 0; }
此時,工作差不多做完了,但還需進行一下設置。在Project|Settings里,把兩個工程里的General標簽里的Intermediate files和Output files都設置為Debug。這樣確保兩個工程的輸出文件在一個目錄中,以便后面動態庫鏈接時的查找。另外,設置DLL_Test為活動工程(Project|Set Active Project),設置DLL_Test依賴於DLL_Lib(Project|Dependencies)。此時,就可以編譯運行了。運行結果為:
> process attach of dll
max(2, 3) = 3
> process detach of dll
Press any key to continue
下面對上面的代碼和結果進行分析。
在dll_lib.h中,EXPORT宏實質上就是一個導出函數所需要的關鍵字。__declspec (dllexport)是Windows擴展關鍵字的組合,表示DLL里的對象的存儲類型關鍵字。extern "C"用於C++程序使用該函數時的函數聲明的鏈接屬性。WINAPI是宏定義,等價於__stdcall。下面列出Windows編程中常見的幾種有關調用約定的宏,它們都是與__stdcall和__cdecl有關的(from windef.h):
#define CALLBACK __stdcall // 用於回調函數
#define WINAPI __stdcall // 用於API函數
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
另外,關於__stdcall:如果通過VC++編寫的DLL欲被其他語言編寫的程序調用,應將函數的調用約定聲明為__stdcall方式,WINAPI、CALLBACK都采用這種方式,而C/C++缺省的調用方式卻為__cdecl。__stdcall方式與__cdecl對函數名最終生成符號的方式不同。若采用C編譯方式(在C++中需將函數聲明為extern "C"),__stdcall調用約定在輸出函數名前面加下划線,后面加“@”符號和參數的字節數,形如_functionName@number ,而__cdecl調用約定僅在輸出函數名前面加下划線,形如_functionName。(小技巧:如何查看這些符號?寫一個程序,只提供函數的聲明而不給定義,就可以看到鏈接器給出的符號了)
因此,在前面例子中,該DLL聲明了一個導出函數GetMax,其連接屬性采用CALLBACK(即__stdcall)。另外,請注意,例子中的宏EXPORT會根據是在C程序還是在C++程序中被調用選擇相應的連接方式。在定義導出函數時,不需要EXPORT宏,只需要在函數聲明時使用即可。
DllMain函數在DLL載入和卸載時被調用。它的第一個參數是DLL句柄,第三個參數保留。第二個參數用來區分該DLLMain函數是在什么情況下被調用的,如程序所示。如果初始化成功,則DllMain應該返回一個非零值。如果返回零值將導致程序停止運行(你可以修改上面例子中的DllMain的返回值為0,將看到相應的出錯結果)。如果在你的DLL程序中沒有編寫DllMain函數,那么在執行該DLL時,系統將引入一個不做任何操作的缺省DllMain函數版本。
/* * testMain.c */ #include <windows.h> #include <stdio.h> typedef int (* PGetMax)(int, int); int main() { int a = 2; int b = 3; HINSTANCE hDll; // DLL句柄 PGetMax pGetMax; // 函數指針 hDll = LoadLibrary(".\\Debug\\DLL_lib.dll"); if (hDll == NULL) { printf("Can't find library file \"dll_lib.dll\"\n"); exit(1); } pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax"); if (pGetMax == NULL) { printf("Can't find function \"GetMax\"\n"); exit(1); } printf(" max(2, 3) = %d\n", pGetMax(2, 3)); FreeLibrary(hDll); return 0; }
此時,不再需要動態的.h文件和.lib文件,只需要提供.dll文件即可。在具體使用時,先用LoadLibrary加載Dll文件,然后用GetProcAddress尋找函數的地址,此時必須提供該函數的在Dll中的名字(不一定與函數名相同)。
然后編譯鏈接、運行,結果與前面的運行結果相同。
下面將解釋,為什么前面要去掉WINAPI調用約定(即采用默認的__cdecl方式)。我們可以先看看DLL_Lib.dll里面的鏈接符號。在cmd中運行命令:
dumpbin /exports DLL_Lib.dll
得到如下結果:
Dump of file f:\code\DLLTest\Debug\Dll_lib.dll File Type: DLL Section contains the following exports for DLL_Lib.dll 0 characteristics 4652C3B1 time date stamp Tue May 22 18:19:29 2007 0.00 version 1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 0000100A GetMax Summary 4000 .data 1000 .idata 3000 .rdata 2000 .reloc 28000 .text
可以看到GetMax函數在編譯后在Dll中的名字仍為GetMax,所以在前面的程序中使用的是:
pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");
然后,我們把WINAPI添加回去,重新編譯DLL_Lib工程。運行剛才的DLL_Test程序,運行出錯,結果如下:
> process attach of dll
Can't find function "GetMax"
> process detach of dll
Press any key to continue
顯然,運行失敗原因是因為沒有找到GetMax函數。再次運行命令:dumpbin /exports DLL_Lib.dll,結果如下(部分結果):
1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 0000100A _GetMax@8
從上面dumpbin的輸出看,GetMax函數在WINAPI調用約定方式下在DLL里的名字與源碼中的函數定義時的名字不再相同,其導出名是"_GetMax@8"。此時,你把testMain.c中的函數指針類型聲明和函數查找語句作如下修改:
typedef int (WINAPI* PGetMax)(int, int);
pGetMax = (PGetMax)GetProcAddress(hDll, "_GetMax@8");
再次編譯鏈接,然后運行,發現結果又正確了。
現在找到了問題所在。很顯然,這種修改方式並不適用,而默認生成的名字又不是我們所想要的。那么該怎么解決這個問題呢?這就需要用到.def文件來解決。
模塊定義文件(.def)
模塊定義文件(.def文件)是一個描述DLL的各種屬性的文件,可以包含一個或多個模塊定義語句。如果你不使用關鍵字__declspec(dllexport)關鍵字導出DLL中的函數,那么DLL就需要一個.def文件。
一個最小的.def文件必須包含下面的模塊定義語句:
(1)文件中第一個語句必須是LIBRARY語句。該語句標記該.def文件屬於哪個DLL。語法形式為:LIBRARY <dll名>。
(2)EXPORTS語句列表。第一個導出語句的形式為:entryname[=internalname] [@ordinal],列出DLL中要導出的函數的名字和可選的序號(ordinal value)。要導出的函數名可以是程序源碼中的函數名,也可以定義新的函數別名(但后面必須緊跟[=<原函數名>]);序號必須在范圍1到N之間且不能重復,其中N是DLL中導出的函數個數。因此,EXPORTS語句語法形式為:
EXPORTS
[=<internalname1] [@<num1="">]
[=<internalname2] [@<num2="">]
;...
(3)雖然不是必須的,一個.def文件也常常包含DESCRIPTION語句,用來描述該DLL的用途之類,語法形式為:
DESCRIPTION ""
(4)在任意位置,可以包含注釋語句,以分號(;)開始。
例如,在本文中后面將用到的.def文件為:
; DLL_Lib.def LIBRARY DLL_Lib ; the dll name DESCRIPTION "Learn how to use the dll." EXPORTS GetMax @1 Max=GetMax @2 ; alias name of GetMax ; Ok, over
現在,讓我們回到DLL_Lib工程,修改GetMax函數的聲明,把EXPORT去掉,重新編譯該工程。然后,運行dumpbin命令,我們發現此時沒有導出函數。再將上面的DLL_Lib.def文件添加進DLL_Lib工程,再次編譯,並運行dumpbin命令,得到如下結果(引用部分結果):
1 ordinal base 2 number of functions 2 number of names ordinal hint RVA name 1 0 0000100A GetMax 2 1 0000100A Max
正如我們所預期的,有兩個導出函數GetMax和Max。注意,此時源碼中的GetMax函數的導出名不再是默認的“_GetMax@8”。另外,需要注意的是,兩個導出函數有相同的相對虛擬地址(RVA),也說明了兩個導出名實質是同一個函數的不同名字而已,都是源碼中GetMax函數的導出名。
現在,回到DLL_Test工程,修改testMain.c文件內容如下:
/* * testMain.c */ #include <windows.h> #include <stdio.h> typedef int (WINAPI* PGetMax)(int, int); int main() { int a = 2; int b = 3; HINSTANCE hDll; // DLL句柄 PGetMax pGetMax; // 函數指針 hDll = LoadLibrary(".\\Debug\\DLL_lib.dll"); if (hDll == NULL) { printf("Can't find library file \"dll_lib.dll\"\n"); exit(1); } pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax"); if (pGetMax == NULL) { printf("Can't find function \"GetMax\"\n"); exit(1); } printf(" GetMax(2, 3) = %d\n", pGetMax(2, 3)); pGetMax = (PGetMax)GetProcAddress(hDll, "Max"); if (pGetMax == NULL) { printf("Can't find function \"GetMax\"\n"); exit(1); } printf(" Max(2, 3) = %d\n", pGetMax(2, 3)); FreeLibrary(hDll); return 0; }
編譯鏈接、運行,結果如下:
> process attach of dll
GetMax(2, 3) = 3
Max(2, 3) = 3
> process detach of dll
Press any key to continue
運行結果正如前面分析的那樣,GetMax和Max都得到了相同的結果。
到這里,我們解決了DLL導出函數名在各種調用約定下的默認名可能不同於源碼中函數名的問題。此時,你就可以制作跟Windows的自帶API函數庫相同的庫了:使用__stdcall調用約定以滿足Windows下的任何語言都可以調用DLL庫,同時使用函數名作為導出名,以方便用戶使用DLL里的函數。
#pragma data_seg ("shared")
int g_oneNumber = 0;
#pragma data_seg ()