燭秋? http://www.cnblogs.com/cswuyg/archive/2011/09/30/dll.html
動態鏈接庫的使用有兩種方式,一種是顯式調用。一種是隱式調用。
(1)?????? 顯式調用:使用LoadLibrary載入動態鏈接庫、使用GetProcAddress獲取某函數地址。
(2)?????? 隱式調用:可以使用#pragma comment(lib, “XX.lib”)的方式,也可以直接將XX.lib加入到工程中。
?
DLL的編寫
編寫dll時,有個重要的問題需要解決,那就是函數重命名——Name-Mangling。解決方式有兩種,一種是直接在代碼里解決采用extent”c”、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另一種是采用def文件。
(1)編寫dll時,為什么有 extern “C”
原因:因為C和C++的重命名規則是不一樣的。這種重命名稱為“Name-Mangling”(名字修飾或名字改編、標識符重命名,有些人翻譯為“名字粉碎法”,這翻譯顯得有些莫名其妙)
據說,C++標準并沒有規定Name-Mangling的方案,所以不同編譯器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的編譯器他們的Name-Mangling規則也是不同的。這樣的話,不同編譯器編譯出來的目標文件.obj 是不通用的,因為同一個函數,使用不同的Name-Mangling在obj文件中就會有不同的名字。如果DLL里的函數重命名規則跟DLL的使用者采用的重命名規則不一致,那就會找不到這個函數。
C標準規定了C語言Name-Mangling的規范(林銳的書有這樣說過)。這樣就使得,任何一個支持C語言的編譯器,它編譯出來的obj文件可以共享,鏈接成可執行文件。這是一種標準,如果DLL跟其使用者都采用這種約定,那么就可以解決函數重命名規則不一致導致的錯誤。
影響符號名的除了C++和C的區別、編譯器的區別之外,還要考慮調用約定導致的Name Mangling。如extern “c” __stdcall的調用方式就會在原來函數名上加上寫表示參數的符號,而extern “c” __cdecl則不會附加額外的符號。
dll中的函數在被調用時是以函數名或函數編號的方式被索引的。這就意味著采用某編譯器的C++的Name-Mangling方式產生的dll文件可能不通用。因為它們的函數名重命名方式不同。為了使得dll可以通用些,很多時候都要使用C的Name-Mangling方式,即是對每一個導出函數聲明為extern “C”,而且采用_stdcall調用約定,接著還需要對導出函數進行重命名,以便導出不加修飾的函數名。
注意到extern “C”的作用是為了解決函數符號名的問題,這對于動態鏈接庫的制造者和動態鏈接庫的使用者都需要遵守的規則。
動態鏈接庫的顯式裝入就是通過GetProcAddress函數,依據動態鏈接庫句柄和函數名,獲取函數地址。因為GetProcAddress僅是操作系統相關,可能會操作各種各樣的編譯器產生的dll,它的參數里的函數名是原原本本的函數名,沒有任何修飾,所以一般情況下需要確保dll’里的函數名是原始的函數名。分兩步:一,如果導出函數使用了extern”C” _cdecl,那么就不需要再重命名了,這個時候dll里的名字就是原始名字;如果使用了extern”C” _stdcall,這時候dll中的函數名被修飾了,就需要重命名。二、重命名的方式有兩種,要么使用*.def文件,在文件外修正,要么使用#pragma,在代碼里給函數別名。
(2)_declspec(dllexport)和_declspec(dllimport)的作用
?????? _declspec還有另外的用途,這里只討論跟dll相關的使用。正如括號里的關鍵字一樣,導出和導入。_declspec(dllexport)用在dll上,用于說明這是導出的函數。而_declspec(dllimport)用在調用dll的程序中,用于說明這是從dll中導入的函數。
?????? 因為dll中必須說明函數要用于導出,所以_declspec(dllexport)很有必要。但是可以換一種方式,可以使用def文件來說明哪些函數用于導出,同時def文件里邊還有函數的編號。
而使用_declspec(dllimport)卻不是必須的,但是建議這么做。因為如果不用_declspec(dllimport)來說明該函數是從dll導入的,那么編譯器就不知道這個函數到底在哪里,生成的exe里會有一個call XX的指令,這個XX是一個常數地址,XX地址處是一個jmp dword ptr[XXXX]的指令,跳轉到該函數的函數體處,顯然這樣就無緣無故多了一次中間的跳轉。如果使用了_declspec(dllimport)來說明,那么就直接產生call dword ptr[XXX],這樣就不會有多余的跳轉了。(參考《加密與解密》第三版279頁)
(3)__stdcall帶來的影響
?????? 這是一種函數的調用方式。默認情況下VC使用的是__cdecl的函數調用方式,如果產生的dll只會給C/C++程序使用,那么就沒必要定義為__stdcall調用方式,如果要給Win32匯編使用(或者其他的__stdcall調用方式的程序),那么就可以使用__stdcall。這個可能不是很重要,因為可以自己在調用函數的時候設置函數調用的規則。像VC就可以設置函數的調用方式,所以可以方便的使用win32匯編產生的dll。不過__stdcall這調用約定會Name-Mangling,所以我覺得用VC默認的調用約定簡便些。但是,如果既要__stdcall調用約定,又要函數名不給修飾,那可以使用*.def文件,或者在代碼里#pragma的方式給函數提供別名(這種方式需要知道修飾后的函數名是什么)。
?
舉例:
?
·extern “C” __declspec(dllexport) bool? __stdcall cswuyg();
·extern “C”__declspec(dllimport) bool __stdcall cswuyg();
?
·#pragma comment(linker, "/export:cswuyg=_cswuyg@0")
?
(4)*.def文件的用途
指定導出函數,并告知編譯器不要以修飾后的函數名作為導出函數名,而以指定的函數名導出函數(比如有函數func,讓編譯器處理后函數名仍為func)。這樣,就可以避免由于microsoft VC++編譯器的獨特處理方式而引起的鏈接錯誤。
也就是說,使用了def文件,那就不需要extern “C”了,也可以不需要__declspec(dllexport)了(不過,dll的制造者除了提供dll之外,還要提供頭文件,需要在頭文件里加上這extern”C”和調用約定,因為使用者需要跟制造者遵守同樣的規則,除非使用者和制造者使用的是同樣的編譯器并對調用約定無特殊要求)。
舉例def文件格式:
LIBRARY? XX(dll名稱這個并不是必須的,但必須確保跟生成的dll名稱一樣)
EXPORTS
[函數名] @ [函數序號]
?
編寫好之后加入到VC的項目中,就可以了。
?????? 另外,要注意的是,如果要使用__stdcall,那么就必須在代碼里使用上__stdcall,因為*.def文件只負責修改函數名稱,不負責調用約定。
也就是說,def文件只管函數名,不管函數平衡堆棧的方式。
?
如果把*.def文件加入到工程之后,鏈接的時候并沒有自動把它加進去。那么可以這樣做:
手動的在link添加:
1)工程的propertiesàConfiguration PropertiesàLinkeràCommand Lineà在“Additional options”里加上:/def:[完整文件名].def
2)工程的propertiesàConfiguration PropertiesàLinkeràInputàModule Definition File里加上[完整文件名].def
?
注意到:即便是使用C的名稱修飾方式,最終產生的函數名稱也可能是會被修飾的。例如,在VC下,_stdcall的調用方式,就會對函數名稱進行修飾,前面加‘_’,后面加上參數相關的其他東西。所以使用*.def文件對函數進行命名很有用,很重要。
(5)、DllMain函數
每一個動態鏈接庫都會有一個DllMain函數。如果在編程的時候沒有定義DllMain函數,那么編譯器會給你加上去。
DllMain函數格式:
BOOL APIENTRY DllMain( HANDLE hModule,
?????????????????????? DWORD? ul_reason_for_call,
?????????????????????? LPVOID lpReserved
???????????????????????????? )
{
???? switch(ul_reason_for_call)
???? {
???? case DLL_PROCESS_ATTACH:
??????????? printf("\nprocess attach of dll");
??????????? break;
???? case DLL_THREAD_ATTACH:
??????????? printf("\nthread attach of dll");
??????????? break;
???? case DLL_THREAD_DETACH:
??????????? printf("\nthread detach of dll");
??????????? break;
???? case DLL_PROCESS_DETACH:
??????????? printf("\nprocess detach of dll");
??????????? break;
???? }
??? return TRUE;
}
(6)、很多都還沒學,如:導出Class、導出變量、DLL更高級的應用。目前先了解點基礎知識。以后補上。
?
?
?
?
?
?
?
?
?
?
2011-8-14補充
?
編寫dll可以使用.def文件對導出的函數名進行命名。
?
1、動態裝入dll,重命名(*.def)的必要性?
因為導出的函數盡可能使用__stdcall的調用方式。而__stdcall的調用方式,無論是C的Name Mangling,還是C++的Name Mangling都會對函數名進行修飾。所以,采用__stdcall調用方式之后,必須使用*.def文件對函數名重命名,不然就不能使用GetProcAddress()通過函數名獲取函數指針。
?
2、隱式調用時,頭文件要注意的地方?
因為使用靜態裝入,需要有頭文件聲明這個要被使用的dll中的函數,如果聲明中指定了__stdcall或者extern “C”,那么在調用這個函數的時候,編譯器就通過Name Mangling之后的函數名去.lib中找這個函數,*.def中的內容是對*.lib里函數的名稱不產生作用,*.def文件里的函數重命名只對dll有用。這就有lib 跟dll里函數名不一致的問題了,但并不會產生影響,DLL的制造者跟使用者采用的是一致函數聲明。
?
3、所以到底要不要使用__stdcall 呢?
我看到一些代碼里是沒有使用__stdcall的。如果不使用__stdcall,而使用默認的調用約定_cdecl,并且有extern ”C”。那么VC是不會任何修飾的。這樣子生成的dll里的函數名就是原來的函數名。也就可以不使用.def文件了。
也有一些要求必須使用__stdcall,例如com相關的東西、系統的回調函數。具體看有沒有需要。
?
?
4、導出函數別名怎么寫?
可以在.def文件里對函數名寫一個別名。
例如:
EXPORTS
cswuygTest(別名) = _showfun@4(要導出的函數)
?
或者:
#pragma comment(linker, "/export:[別名] =[NameMangling后的名稱]")
?
這樣做就可以隨便修改別名了,不會出現找不到符號的錯誤。
?
5、用不用*.def文件?
如果采用VC默認的調用約定,可以不用*.def文件,如果要采用__stdcall調用約定,又不想函數名被修飾,那就采用*.def文件吧,另一種在代碼里寫的重命名的方式不夠方便。
6、什么情況下(不)需要考慮函數重命名的問題?
1)、隱式調用(通過lib)
如果dll的制造者跟dll的使用者采用同樣的語言、同樣編程環境,那么就不需要考慮函數重命名。使用者在調用函數的時候,通過Name Mangling后的函數名能在lib里找到該函數。
如果dll的制造者跟dll使用不同的語言、或者不同的編譯器,那就需要考慮重命名了。
2)、顯示調用(通過GetProcessAddress)
?????? 這絕對是必須考慮函數重命名的。
7、總結
??? 總的來說,在編寫DLL的時候,寫個頭文件,頭文件里聲明函數的NameMingling方式、調用約定(主要是為了隱式調用)。再寫個*.def文件把函數重命名了(主要是為了顯式調用)。提供*.DLL\*.lib\*.h給dll的使用者,這樣無論是隱式的調用,還是顯式的調用,都可以方便的進行。
?
附:
一個簡單DLL導出函數的例子:http://files.cnblogs.com/cswuyg/%E7%BC%96%E5%86%99DLL%E6%89%80%E5%AD%A6%E6%89%80%E6%80%9D.rar
?
學習資料:
http://www.cnblogs.com/dongzhiquan/archive/2009/08/04/1994764.html
http://topic.csdn.net/u/20081126/14/70ac75b3-6e79-4c48-b9fe-918dce147484.html
?