exe調用DLL的方式
- 2020 年 8 月 19 日
- 筆記
- IT-程式設計-C/C++
假設被調用的DLL存在一個導出函數,原型如下:
void printN(int);
三種方式從DLL導入導出函數
- 生成
DLL
時使用模組定義 (.def
) 文件 - 在主應用程式的函數定義中使用關鍵字
__declspec(dllimport)
或__declspec(dllexport)
- 利用
#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"
def編寫規範:參考模組定義 (.Def) 文件
基本規則:
- LIBRARY 語句說明 .def ⽂件相應的 DLL;
- EXPORTS 語句後列出要導出函數的名稱。可以在 .def ⽂件中的導出函數名後加 @n,表 示要導出函數的序號為 n(在進⾏函數調⽤時,這個序號將發揮其作⽤);
- .def ⽂件中的注釋由每個注釋⾏開始處的分號 (
指定,且注釋不能與語句共享⼀⾏。
編寫dll注意點
編寫dll時,有個重要的問題需要解決,那就是函數重命名——Name-Mangling
。解決方式有兩種,一種是直接在程式碼里解決採用extent」c」
、_declspec(dllexport)
、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]")
,另一種是採用def
文件。
編寫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。如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
,在程式碼里給函數別名。
_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]
,這樣就不會有多餘的跳轉了。
__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")
編寫測試dll程式碼
項目結構:
cpp源程式碼:
#include <iostream>
using namespace std;
extern "C" {
_declspec(dllexport) void printN(int n)
{
//printf("%d\n", n);
cout << n << endl;
}
}
void printM(int m)
{
cout << m << endl;
}
#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")
int getNresult()
{
//printf("%d\n", n);
return 123;
}
def程式碼:
LIBRARY DLLTEST
EXPORTS
printM
項目屬性中將配置類型改為dll:
模組定義文件改為dlltest.def:
編譯之後,使用CFF Explorer查看導出函數:
其中printN
函數用extern "C" _declspec(dllexport)
的方式導出,避免了函數名粉碎;
printM
函數用def
的形式導出,也避免了函數名粉碎;
getNresult
函數用#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")
的形式避免了函數名粉碎,但是需要知道粉碎後的原始函數符號;
這裡涉及一個問題,原始函數符號怎麼找到的,方法是先用_declspec(dllexport)
方式導出,然後編譯後利用CFF即可看到原始函數符號。
編譯dll後會產生一個dll文件和一個lib文件,如果是運行時動態調用的方式只使用dll文件就行,如果要在編譯時以庫的形式提供給exe調用則需要lib文件。
編寫exe調用dll
項目結構:
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)
形式導入,getNresult
和printM
是c++格式的,應該使用__declspec(dllimport)
導入,不過導入函數的情況下可以省略不寫,引用外部變數則不能省略。
執行結果: