编写DLL所学所思(1)——导出函数
燭秋? http://www.cnblogs.com/cswuyg/archive/2011/09/30/dll.html
動(dòng)態(tài)鏈接庫的使用有兩種方式,一種是顯式調(diào)用。一種是隱式調(diào)用。
(1)?????? 顯式調(diào)用:使用LoadLibrary載入動(dòng)態(tài)鏈接庫、使用GetProcAddress獲取某函數(shù)地址。
(2)?????? 隱式調(diào)用:可以使用#pragma comment(lib, “XX.lib”)的方式,也可以直接將XX.lib加入到工程中。
?
DLL的編寫
編寫dll時(shí),有個(gè)重要的問題需要解決,那就是函數(shù)重命名——Name-Mangling。解決方式有兩種,一種是直接在代碼里解決采用extent”c”、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另一種是采用def文件。
(1)編寫dll時(shí),為什么有 extern “C”
原因:因?yàn)镃和C++的重命名規(guī)則是不一樣的。這種重命名稱為“Name-Mangling”(名字修飾或名字改編、標(biāo)識(shí)符重命名,有些人翻譯為“名字粉碎法”,這翻譯顯得有些莫名其妙)
據(jù)說,C++標(biāo)準(zhǔn)并沒有規(guī)定Name-Mangling的方案,所以不同編譯器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的編譯器他們的Name-Mangling規(guī)則也是不同的。這樣的話,不同編譯器編譯出來的目標(biāo)文件.obj 是不通用的,因?yàn)橥粋€(gè)函數(shù),使用不同的Name-Mangling在obj文件中就會(huì)有不同的名字。如果DLL里的函數(shù)重命名規(guī)則跟DLL的使用者采用的重命名規(guī)則不一致,那就會(huì)找不到這個(gè)函數(shù)。
C標(biāo)準(zhǔn)規(guī)定了C語言Name-Mangling的規(guī)范(林銳的書有這樣說過)。這樣就使得,任何一個(gè)支持C語言的編譯器,它編譯出來的obj文件可以共享,鏈接成可執(zhí)行文件。這是一種標(biāo)準(zhǔn),如果DLL跟其使用者都采用這種約定,那么就可以解決函數(shù)重命名規(guī)則不一致導(dǎo)致的錯(cuò)誤。
影響符號(hào)名的除了C++和C的區(qū)別、編譯器的區(qū)別之外,還要考慮調(diào)用約定導(dǎo)致的Name Mangling。如extern “c” __stdcall的調(diào)用方式就會(huì)在原來函數(shù)名上加上寫表示參數(shù)的符號(hào),而extern “c” __cdecl則不會(huì)附加額外的符號(hào)。
dll中的函數(shù)在被調(diào)用時(shí)是以函數(shù)名或函數(shù)編號(hào)的方式被索引的。這就意味著采用某編譯器的C++的Name-Mangling方式產(chǎn)生的dll文件可能不通用。因?yàn)樗鼈兊暮瘮?shù)名重命名方式不同。為了使得dll可以通用些,很多時(shí)候都要使用C的Name-Mangling方式,即是對(duì)每一個(gè)導(dǎo)出函數(shù)聲明為extern “C”,而且采用_stdcall調(diào)用約定,接著還需要對(duì)導(dǎo)出函數(shù)進(jìn)行重命名,以便導(dǎo)出不加修飾的函數(shù)名。
注意到extern “C”的作用是為了解決函數(shù)符號(hào)名的問題,這對(duì)于動(dòng)態(tài)鏈接庫的制造者和動(dòng)態(tài)鏈接庫的使用者都需要遵守的規(guī)則。
動(dòng)態(tài)鏈接庫的顯式裝入就是通過GetProcAddress函數(shù),依據(jù)動(dòng)態(tài)鏈接庫句柄和函數(shù)名,獲取函數(shù)地址。因?yàn)镚etProcAddress僅是操作系統(tǒng)相關(guān),可能會(huì)操作各種各樣的編譯器產(chǎn)生的dll,它的參數(shù)里的函數(shù)名是原原本本的函數(shù)名,沒有任何修飾,所以一般情況下需要確保dll’里的函數(shù)名是原始的函數(shù)名。分兩步:一,如果導(dǎo)出函數(shù)使用了extern”C” _cdecl,那么就不需要再重命名了,這個(gè)時(shí)候dll里的名字就是原始名字;如果使用了extern”C” _stdcall,這時(shí)候dll中的函數(shù)名被修飾了,就需要重命名。二、重命名的方式有兩種,要么使用*.def文件,在文件外修正,要么使用#pragma,在代碼里給函數(shù)別名。
(2)_declspec(dllexport)和_declspec(dllimport)的作用
?????? _declspec還有另外的用途,這里只討論跟dll相關(guān)的使用。正如括號(hào)里的關(guān)鍵字一樣,導(dǎo)出和導(dǎo)入。_declspec(dllexport)用在dll上,用于說明這是導(dǎo)出的函數(shù)。而_declspec(dllimport)用在調(diào)用dll的程序中,用于說明這是從dll中導(dǎo)入的函數(shù)。
?????? 因?yàn)閐ll中必須說明函數(shù)要用于導(dǎo)出,所以_declspec(dllexport)很有必要。但是可以換一種方式,可以使用def文件來說明哪些函數(shù)用于導(dǎo)出,同時(shí)def文件里邊還有函數(shù)的編號(hào)。
而使用_declspec(dllimport)卻不是必須的,但是建議這么做。因?yàn)槿绻挥胈declspec(dllimport)來說明該函數(shù)是從dll導(dǎo)入的,那么編譯器就不知道這個(gè)函數(shù)到底在哪里,生成的exe里會(huì)有一個(gè)call XX的指令,這個(gè)XX是一個(gè)常數(shù)地址,XX地址處是一個(gè)jmp dword ptr[XXXX]的指令,跳轉(zhuǎn)到該函數(shù)的函數(shù)體處,顯然這樣就無緣無故多了一次中間的跳轉(zhuǎn)。如果使用了_declspec(dllimport)來說明,那么就直接產(chǎn)生call dword ptr[XXX],這樣就不會(huì)有多余的跳轉(zhuǎn)了。(參考《加密與解密》第三版279頁)
(3)__stdcall帶來的影響
?????? 這是一種函數(shù)的調(diào)用方式。默認(rèn)情況下VC使用的是__cdecl的函數(shù)調(diào)用方式,如果產(chǎn)生的dll只會(huì)給C/C++程序使用,那么就沒必要定義為__stdcall調(diào)用方式,如果要給Win32匯編使用(或者其他的__stdcall調(diào)用方式的程序),那么就可以使用__stdcall。這個(gè)可能不是很重要,因?yàn)榭梢宰约涸谡{(diào)用函數(shù)的時(shí)候設(shè)置函數(shù)調(diào)用的規(guī)則。像VC就可以設(shè)置函數(shù)的調(diào)用方式,所以可以方便的使用win32匯編產(chǎn)生的dll。不過__stdcall這調(diào)用約定會(huì)Name-Mangling,所以我覺得用VC默認(rèn)的調(diào)用約定簡便些。但是,如果既要__stdcall調(diào)用約定,又要函數(shù)名不給修飾,那可以使用*.def文件,或者在代碼里#pragma的方式給函數(shù)提供別名(這種方式需要知道修飾后的函數(shù)名是什么)。
?
舉例:
?
·extern “C” __declspec(dllexport) bool? __stdcall cswuyg();
·extern “C”__declspec(dllimport) bool __stdcall cswuyg();
?
·#pragma comment(linker, "/export:cswuyg=_cswuyg@0")
?
(4)*.def文件的用途
指定導(dǎo)出函數(shù),并告知編譯器不要以修飾后的函數(shù)名作為導(dǎo)出函數(shù)名,而以指定的函數(shù)名導(dǎo)出函數(shù)(比如有函數(shù)func,讓編譯器處理后函數(shù)名仍為func)。這樣,就可以避免由于microsoft VC++編譯器的獨(dú)特處理方式而引起的鏈接錯(cuò)誤。
也就是說,使用了def文件,那就不需要extern “C”了,也可以不需要__declspec(dllexport)了(不過,dll的制造者除了提供dll之外,還要提供頭文件,需要在頭文件里加上這extern”C”和調(diào)用約定,因?yàn)槭褂谜咝枰圃煺咦袷赝瑯拥囊?guī)則,除非使用者和制造者使用的是同樣的編譯器并對(duì)調(diào)用約定無特殊要求)。
舉例def文件格式:
LIBRARY? XX(dll名稱這個(gè)并不是必須的,但必須確保跟生成的dll名稱一樣)
EXPORTS
[函數(shù)名] @ [函數(shù)序號(hào)]
?
編寫好之后加入到VC的項(xiàng)目中,就可以了。
?????? 另外,要注意的是,如果要使用__stdcall,那么就必須在代碼里使用上__stdcall,因?yàn)?.def文件只負(fù)責(zé)修改函數(shù)名稱,不負(fù)責(zé)調(diào)用約定。
也就是說,def文件只管函數(shù)名,不管函數(shù)平衡堆棧的方式。
?
如果把*.def文件加入到工程之后,鏈接的時(shí)候并沒有自動(dòng)把它加進(jìn)去。那么可以這樣做:
手動(dòng)的在link添加:
1)工程的propertiesàConfiguration PropertiesàLinkeràCommand Lineà在“Additional options”里加上:/def:[完整文件名].def
2)工程的propertiesàConfiguration PropertiesàLinkeràInputàModule Definition File里加上[完整文件名].def
?
注意到:即便是使用C的名稱修飾方式,最終產(chǎn)生的函數(shù)名稱也可能是會(huì)被修飾的。例如,在VC下,_stdcall的調(diào)用方式,就會(huì)對(duì)函數(shù)名稱進(jìn)行修飾,前面加‘_’,后面加上參數(shù)相關(guān)的其他東西。所以使用*.def文件對(duì)函數(shù)進(jìn)行命名很有用,很重要。
(5)、DllMain函數(shù)
每一個(gè)動(dòng)態(tài)鏈接庫都會(huì)有一個(gè)DllMain函數(shù)。如果在編程的時(shí)候沒有定義DllMain函數(shù),那么編譯器會(huì)給你加上去。
DllMain函數(shù)格式:
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)、很多都還沒學(xué),如:導(dǎo)出Class、導(dǎo)出變量、DLL更高級(jí)的應(yīng)用。目前先了解點(diǎn)基礎(chǔ)知識(shí)。以后補(bǔ)上。
?
?
?
?
?
?
?
?
?
?
2011-8-14補(bǔ)充
?
編寫dll可以使用.def文件對(duì)導(dǎo)出的函數(shù)名進(jìn)行命名。
?
1、動(dòng)態(tài)裝入dll,重命名(*.def)的必要性?
因?yàn)閷?dǎo)出的函數(shù)盡可能使用__stdcall的調(diào)用方式。而__stdcall的調(diào)用方式,無論是C的Name Mangling,還是C++的Name Mangling都會(huì)對(duì)函數(shù)名進(jìn)行修飾。所以,采用__stdcall調(diào)用方式之后,必須使用*.def文件對(duì)函數(shù)名重命名,不然就不能使用GetProcAddress()通過函數(shù)名獲取函數(shù)指針。
?
2、隱式調(diào)用時(shí),頭文件要注意的地方?
因?yàn)槭褂渺o態(tài)裝入,需要有頭文件聲明這個(gè)要被使用的dll中的函數(shù),如果聲明中指定了__stdcall或者extern “C”,那么在調(diào)用這個(gè)函數(shù)的時(shí)候,編譯器就通過Name Mangling之后的函數(shù)名去.lib中找這個(gè)函數(shù),*.def中的內(nèi)容是對(duì)*.lib里函數(shù)的名稱不產(chǎn)生作用,*.def文件里的函數(shù)重命名只對(duì)dll有用。這就有l(wèi)ib 跟dll里函數(shù)名不一致的問題了,但并不會(huì)產(chǎn)生影響,DLL的制造者跟使用者采用的是一致函數(shù)聲明。
?
3、所以到底要不要使用__stdcall 呢?
我看到一些代碼里是沒有使用__stdcall的。如果不使用__stdcall,而使用默認(rèn)的調(diào)用約定_cdecl,并且有extern ”C”。那么VC是不會(huì)任何修飾的。這樣子生成的dll里的函數(shù)名就是原來的函數(shù)名。也就可以不使用.def文件了。
也有一些要求必須使用__stdcall,例如com相關(guān)的東西、系統(tǒng)的回調(diào)函數(shù)。具體看有沒有需要。
?
?
4、導(dǎo)出函數(shù)別名怎么寫?
可以在.def文件里對(duì)函數(shù)名寫一個(gè)別名。
例如:
EXPORTS
cswuygTest(別名) = _showfun@4(要導(dǎo)出的函數(shù))
?
或者:
#pragma comment(linker, "/export:[別名] =[NameMangling后的名稱]")
?
這樣做就可以隨便修改別名了,不會(huì)出現(xiàn)找不到符號(hào)的錯(cuò)誤。
?
5、用不用*.def文件?
如果采用VC默認(rèn)的調(diào)用約定,可以不用*.def文件,如果要采用__stdcall調(diào)用約定,又不想函數(shù)名被修飾,那就采用*.def文件吧,另一種在代碼里寫的重命名的方式不夠方便。
6、什么情況下(不)需要考慮函數(shù)重命名的問題?
1)、隱式調(diào)用(通過lib)
如果dll的制造者跟dll的使用者采用同樣的語言、同樣編程環(huán)境,那么就不需要考慮函數(shù)重命名。使用者在調(diào)用函數(shù)的時(shí)候,通過Name Mangling后的函數(shù)名能在lib里找到該函數(shù)。
如果dll的制造者跟dll使用不同的語言、或者不同的編譯器,那就需要考慮重命名了。
2)、顯示調(diào)用(通過GetProcessAddress)
?????? 這絕對(duì)是必須考慮函數(shù)重命名的。
7、總結(jié)
??? 總的來說,在編寫DLL的時(shí)候,寫個(gè)頭文件,頭文件里聲明函數(shù)的NameMingling方式、調(diào)用約定(主要是為了隱式調(diào)用)。再寫個(gè)*.def文件把函數(shù)重命名了(主要是為了顯式調(diào)用)。提供*.DLL\*.lib\*.h給dll的使用者,這樣無論是隱式的調(diào)用,還是顯式的調(diào)用,都可以方便的進(jìn)行。
?
附:
一個(gè)簡單DLL導(dǎo)出函數(shù)的例子:http://files.cnblogs.com/cswuyg/%E7%BC%96%E5%86%99DLL%E6%89%80%E5%AD%A6%E6%89%80%E6%80%9D.rar
?
學(xué)習(xí)資料:
http://www.cnblogs.com/dongzhiquan/archive/2009/08/04/1994764.html
http://topic.csdn.net/u/20081126/14/70ac75b3-6e79-4c48-b9fe-918dce147484.html
?
轉(zhuǎn)載于:https://www.cnblogs.com/adder01/p/4737935.html
總結(jié)
以上是生活随笔為你收集整理的编写DLL所学所思(1)——导出函数的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android红米3调用相机,红米3有什
- 下一篇: java缓存技术的介绍