假設(shè)被調(diào)用的DLL存在一個(gè)導(dǎo)出函數(shù),原型如下:
void printN(int);
1|0三種方式從DLL導(dǎo)入導(dǎo)出函數(shù)
- 生成DLL時(shí)使用模塊定義 (.def) 文件
- 在主應(yīng)用程序的函數(shù)定義中使用關(guān)鍵字__declspec(dllimport)或__declspec(dllexport)
- 利用#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"
def編寫規(guī)范:參考模塊定義 (.Def) 文件
基本規(guī)則:
- LIBRARY 語(yǔ)句說(shuō)明 .def ?件相應(yīng)的 DLL;
- EXPORTS 語(yǔ)句后列出要導(dǎo)出函數(shù)的名稱。可以在 .def ?件中的導(dǎo)出函數(shù)名后加 @n,表 示要導(dǎo)出函數(shù)的序號(hào)為 n(在進(jìn)?函數(shù)調(diào)?時(shí),這個(gè)序號(hào)將發(fā)揮其作?);
- .def ?件中的注釋由每個(gè)注釋?開始處的分號(hào) ( 指定,且注釋不能與語(yǔ)句共享??。
2|0編寫dll注意點(diǎn)
編寫dll時(shí),有個(gè)重要的問(wèn)題需要解決,那就是函數(shù)重命名——Name-Mangling。解決方式有兩種,一種是直接在代碼里解決采用extent”c”、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另一種是采用def文件。
2|1編寫dll時(shí),為什么有 extern “C”
原因:因?yàn)镃和C++的重命名規(guī)則是不一樣的。這種重命名稱為“Name-Mangling”(名字修飾或名字改編、標(biāo)識(shí)符重命名,有些人翻譯為“名字粉碎法”,這翻譯顯得有些莫名其妙)
據(jù)說(shuō),C++標(biāo)準(zhǔn)并沒(méi)有規(guī)定Name-Mangling的方案,所以不同編譯器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的編譯器他們的Name-Mangling規(guī)則也是不同的。這樣的話,不同編譯器編譯出來(lái)的目標(biāo)文件.obj 是不通用的,因?yàn)橥粋€(gè)函數(shù),使用不同的Name-Mangling在obj文件中就會(huì)有不同的名字。如果DLL里的函數(shù)重命名規(guī)則跟DLL的使用者采用的重命名規(guī)則不一致,那就會(huì)找不到這個(gè)函數(shù)。
影響符號(hào)名的除了C++和C的區(qū)別、編譯器的區(qū)別之外,還要考慮調(diào)用約定導(dǎo)致的Name Mangling。如extern “c” __stdcall的調(diào)用方式就會(huì)在原來(lái)函數(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)名的問(wèn)題,這對(duì)于動(dòng)態(tài)鏈接庫(kù)的制造者和動(dòng)態(tài)鏈接庫(kù)的使用者都需要遵守的規(guī)則。
動(dòng)態(tài)鏈接庫(kù)的顯式裝入就是通過(guò)GetProcAddress函數(shù),依據(jù)動(dòng)態(tài)鏈接庫(kù)句柄和函數(shù)名,獲取函數(shù)地址。因?yàn)镚etProcAddress僅是操作系統(tǒng)相關(guān),可能會(huì)操作各種各樣的編譯器產(chǎn)生的dll,它的參數(shù)里的函數(shù)名是原原本本的函數(shù)名,沒(méi)有任何修飾,所以一般情況下需要確保dll里的函數(shù)名是原始的函數(shù)名。分兩步:
一,如果導(dǎo)出函數(shù)使用了extern”C” _cdecl,那么就不需要再重命名了,這個(gè)時(shí)候dll里的名字就是原始名字;如果使用了extern”C” _stdcall,這時(shí)候dll中的函數(shù)名被修飾了,就需要重命名。
二、重命名的方式有兩種,要么使用*.def文件,在文件外修正,要么使用#pragma,在代碼里給函數(shù)別名。
2|2_declspec(dllexport)和_declspec(dllimport)的作用
_declspec還有另外的用途,這里只討論跟dll相關(guān)的使用。正如括號(hào)里的關(guān)鍵字一樣,導(dǎo)出和導(dǎo)入。_declspec(dllexport)用在dll上,用于說(shuō)明這是導(dǎo)出的函數(shù)。而_declspec(dllimport)用在調(diào)用dll的程序中,用于說(shuō)明這是從dll中導(dǎo)入的函數(shù)。
因?yàn)閐ll中必須說(shuō)明函數(shù)要用于導(dǎo)出,所以_declspec(dllexport)很有必要。但是可以換一種方式,可以使用def文件來(lái)說(shuō)明哪些函數(shù)用于導(dǎo)出,同時(shí)def文件里邊還有函數(shù)的編號(hào)。
而使用_declspec(dllimport)卻不是必須的,但是建議這么做。因?yàn)槿绻挥胈declspec(dllimport)來(lái)說(shuō)明該函數(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ù)體處,顯然這樣就無(wú)緣無(wú)故多了一次中間的跳轉(zhuǎn)。如果使用了_declspec(dllimport)來(lái)說(shuō)明,那么就直接產(chǎn)生call dword ptr[XXX],這樣就不會(huì)有多余的跳轉(zhuǎn)了。
2|3__stdcall帶來(lái)的影響
這是一種函數(shù)的調(diào)用方式。默認(rèn)情況下VC使用的是__cdecl的函數(shù)調(diào)用方式,如果產(chǎn)生的dll只會(huì)給C/C++程序使用,那么就沒(méi)必要定義為__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。不過(guò)__stdcall這調(diào)用約定會(huì)Name-Mangling,所以我覺(jué)得用VC默認(rèn)的調(diào)用約定簡(jiǎn)便些。但是,如果既要__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")
3|0編寫測(cè)試dll代碼
項(xiàng)目結(jié)構(gòu):
cpp源代碼:
#include <IOStream>
using namespace std;
extern "C" {
_declspec(dllexport) void printN(int n)
{
//printf("%dn", n);
cout << n << endl;
}
}
void printM(int m)
{
cout << m << endl;
}
#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")
int getNresult()
{
//printf("%dn", n);
return 123;
}
def代碼:
LIBRARY DLLTEST
EXPORTS
printM
項(xiàng)目屬性中將配置類型改為dll:
模塊定義文件改為dlltest.def:
編譯之后,使用CFF Explorer查看導(dǎo)出函數(shù):
其中printN函數(shù)用extern "C" _declspec(dllexport)的方式導(dǎo)出,避免了函數(shù)名粉碎;
printM函數(shù)用def的形式導(dǎo)出,也避免了函數(shù)名粉碎;
getNresult函數(shù)用#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")的形式避免了函數(shù)名粉碎,但是需要知道粉碎后的原始函數(shù)符號(hào);
這里涉及一個(gè)問(wèn)題,原始函數(shù)符號(hào)怎么找到的,方法是先用_declspec(dllexport)方式導(dǎo)出,然后編譯后利用CFF即可看到原始函數(shù)符號(hào)。
編譯dll后會(huì)產(chǎn)生一個(gè)dll文件和一個(gè)lib文件,如果是運(yùn)行時(shí)動(dòng)態(tài)調(diào)用的方式只使用dll文件就行,如果要在編譯時(shí)以庫(kù)的形式提供給exe調(diào)用則需要lib文件。
4|0編寫exe調(diào)用dll
項(xiàng)目結(jié)構(gòu):
cpp源碼:
#include <iostream>
using namespace std;
#pragma comment(lib, "C:\project\dlltest\Debug\dlltest.lib")
extern "C" __declspec(dllimport) void printN(int);
int getNresult();
void printM(int);
int main()
{
printN(123);
printM(12);
cout << getNresult() << endl;
return 0;
}
在#pragma中更改為自己的lib路徑,printN與extern "C" __declspec(dllimport)形式導(dǎo)入,getNresult和printM是c++格式的,應(yīng)該使用__declspec(dllimport)導(dǎo)入,不過(guò)導(dǎo)入函數(shù)的情況下可以省略不寫,引用外部變量則不能省略。
執(zhí)行結(jié)果:
5|0利用LoadLibrary動(dòng)態(tài)加載dll的方式
這種方式需要明確指定dll的位置,而不是程序根據(jù)環(huán)境變量配置自己尋找(上面的方式中并沒(méi)有指明dll的位置,exe和dll同目錄會(huì)自動(dòng)搜索加載)。
代碼:
#include <iostream>
#include <windows.h>
using namespace std;
int main()
{
HINSTANCE h = LoadLibrary(L"C:\project\dlltest\Debug\dlltest.dll");
if (h == NULL)
{
cout << "dll加載失敗!" << endl;
}
else
{
void* func = GetProcAddress(h, "printN");
if (func != NULL)
{
((void(*)(int))func)(2);
}
else
{
cout << "未找到相關(guān)函數(shù)!" << endl;
}
}
return 0;
}
需要注意將項(xiàng)目的字符集改為Unicode:






