exe呼叫DLL的方式
阿新 • • 發佈:2020-08-19
假設被呼叫的DLL存在一個匯出函式,原型如下:
```c
void printN(int);
```
# 三種方式從DLL匯入匯出函式
- 生成`DLL`時使用模組定義 (`.def`) 檔案
- 在主應用程式的函式定義中使用關鍵字`__declspec(dllimport)`或`__declspec(dllexport)`
- 利用`#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"`
def編寫規範:參考[模組定義 (.Def) 檔案](https://docs.microsoft.com/zh-cn/cpp/build/reference/module-definition-dot-def-files?view=vs-2019)
基本規則:
- 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的方式給函式提供別名(這種方式需要知道修飾後的函式名是什麼)。
舉例:
```c++
·extern “C” __declspec(dllexport) bool __stdcall cswuyg();
·extern “C”__declspec(dllimport) bool __stdcall cswuyg();
·#pragma comment(linker, "/export:cswuyg=_cswuyg@0")
```
# 編寫測試dll程式碼
專案結構:
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819144714.png)
cpp原始碼:
```c++
#include
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:
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819145122.png)
模組定義檔案改為dlltest.def:
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819145251.png)
編譯之後,使用CFF Explorer檢視匯出函式:
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819145437.png)
其中`printN`函式用`extern "C" _declspec(dllexport)`的方式匯出,避免了函式名粉碎;
`printM`函式用`def`的形式匯出,也避免了函式名粉碎;
`getNresult`函式用`#pragma comment(linker, "/export:getNresult=?getNresult@@YAHXZ")`的形式避免了函式名粉碎,但是需要知道粉碎後的原始函式符號;
這裡涉及一個問題,原始函式符號怎麼找到的,方法是先用`_declspec(dllexport)`方式匯出,然後編譯後利用CFF即可看到原始函式符號。
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819150146.png)
編譯dll後會產生一個dll檔案和一個lib檔案,如果是執行時動態呼叫的方式只使用dll檔案就行,如果要在編譯時以庫的形式提供給exe呼叫則需要lib檔案。
# 編寫exe呼叫dll
專案結構:
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819150714.png)
cpp原始碼:
```c++
#include
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)`匯入,不過匯入函式的情況下可以省略不寫,引用外部變數則不能省略。
執行結果:
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819152123.png)
# 利用LoadLibrary動態載入dll的方式
這種方式需要明確指定dll的位置,而不是程式根據環境變數配置自己尋找(上面的方式中並沒有指明dll的位置,exe和dll同目錄會自動搜尋載入)。
程式碼:
```c++
#include
#include
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 << "未找到相關函式!" << endl;
}
}
return 0;
}
```
需要注意將專案的字符集改為`Unicode`:
![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200819154635.png)