dll的def檔案與__declspec(dllexport)匯出函式方式比較

查志強發表於2016-06-03

【原文:http://www.xuebuyuan.com/1752443.html

dll的def檔案與__declspec(dllexport)匯出函式方式比較

【__declspec(dllexport) 方式】
首先對C和C++編譯(extern "C")與呼叫約定(__cdecl、__stdcall、__fastcall)進行組合測試:
【C++編譯】
__declspec(dllexport) int add(int, int);

__declspec(dllexport) int __cdecl add(int, int);

__declspec(dllexport) int __stdcall add(int, int);

__declspec(dllexport) int __fastcall add(int, int);

對於C++編譯器的函式名修飾規則:不管__cdecl, __fastcall還是__stdcall呼叫方式,函式修飾名都是以"?"開始,後面是函式在名字,再後面是函式返回型別和引數型別按照代號拼出的參數列。對於__stdcall方式,參數列的開始標示是"@@YG”,對於__cdecl方式則是"@@YA”,對於__fastcall方式則是"@@YI”.
參數列後以"@Z”標示整個名字的結束,如果該函式無引數,則以"Z”標識結束。 
更詳細的dll基礎知識請參考:
http://hi.baidu.com/luosiyong/blog/item/92bbdcfe860435375c600812.html
更深入的C++函式名修飾編碼規則請參考:
http://hi.baidu.com/wanggang2008/blog/item/cd43e60756021bc07a89470a.html


【C編譯】
extern "C" __declspec(dllexport) int add(int, int);

extern "C" __declspec(dllexport) int __cdecl add(int, int);

extern "C" __declspec(dllexport) int __stdcall add(int, int);

extern "C" __declspec(dllexport) int __fastcall add(int, int);

如果建立dll和可執行檔案都是使用的VC,那用__declspec(dllexport)足夠解決問題。但是如果建立出來的dll要和別的廠商的工具包構建的可執行檔案連結,那就有一些額外的問題來了。
在開發dll的時候,一般不讓編譯器改變函式名,所以通常是以C方式編譯,即加入了extern "C"說明。但是看上面的組合測試結果,__stdcall和__fastcall編譯出來的函式名還是和原始的函式名不同。就拿__stdcall來說,它以C編譯匯出的時候,會在函式前面加入下劃線,並在函式後面加入@和引數總大小的位元組數。
或許現在你就想,__cdecl不就是沒有改變名稱的方式嗎,並且預設也是__cdecl呼叫約定,的確,我們自己寫的dll幾乎都可以使用__cdecl方式。但是,Windows中最普遍的呼叫方式都是__stdcall(比如CALLBACK、 WINAPI),一些常用的定義如下:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
現在假如用VC的__stdcall方式開發的一個dll,裡面包含了上面那樣的add函式,如果要在VB中使用,VB的程式員需要如下宣告:
Declare Function add Lib "win.dll" Alias"_add@8"() As Integer
注意他需要寫的名稱是 "_add@8",而不是簡單的"add",否則就會出現函式未定義的連結錯誤。
【備註】
__declspec(dllexport)的位置:
To export functions, the __declspec(dllexport) keyword must appear to the left of the calling-convention keyword, if a keyword is specified.

For example:
__declspec(dllexport) void __cdecl Function1(void);


To export all of the public data members and member functions in a class, the keyword must appear to the left of the class name as follows:
class __declspec(dllexport) CExampleExport : public CObject

{  class definition  };
Reference

1. 
http://msdn.microsoft.com/en-us/library/d91k01sh(VS.80).aspx

2. 
http://msdn.microsoft.com/en-us/library/a90k134d(VS.80).aspx

【def檔案匯出方式】
首先了解一下 使用def檔案從dll匯出:
http://msdn.microsoft.com/zh-cn/library/d91k01sh(v=VS.80).aspx

具體到測試例項,我們的def檔案內容如下:
LIBRARY "win"

EXPORTS
add @1

其中LIBRARY指定dll的模組名稱,即dll名字,EXPORTS後的每一行指定一個匯出函式名字,這個名字和標頭檔案中的宣告一致,後面可以跟@序號指定該函式的序號(這個是可選的,後面按序號匯入函式的時候再詳細說)。

然後再測試一下__stdcall和__fastcall是否會對匯出函式改名,測試結果如下,均未改名:

extern "C" int __stdcall add();

extern "C" int __fastcall add();

另外一種方案是在程式碼中給連結器指定匯出函式名字:
extern "C" __declspec(dllexport) int __fastcall add(int a, int b);
#pragma comment(linker, "/export:add=@add@8")
這裡告訴連結器,匯出一個函式名為add的函式,函式入口點和@add@8相同

這樣,我們既可以使用add,也可以使用@add@8了。
__stdcall方式和這類似,為add=_add@8。

【按序號而不是按名稱從dll匯出函式】
http://msdn.microsoft.com/zh-cn/library/e7tsx612%28VS.80%29.aspx
def檔案定義如下:
LIBRARY "win"

EXPORTS
add @1 NONAME

函式名稱和Hint都不見了。
這樣也可以用來隱藏dll中一些重要函式。

隱藏了函式名稱,在應用程式中使用序號來匯入函式:
#include <windows.h>
#include <stdio.h>

int main()
{
typedef int (* AddFunc)(int, int);
HMODULE hModule = LoadLibrary("dll.dll");
AddFunc add = (AddFunc)GetProcAddress(hModule, MAKEINTRESOURCE(1)); //
注意這裡序號的指定方式
printf("%d\n", add(1, 2));
return 0;
}

【備註】
def
檔案和__declspec(dllexport)方式優缺點對比:

一、__declspec(dllexport)

在 32 位編譯器版本中,可以使用 __declspec(dllexport) 關鍵字從 DLL 匯出資料、函式、類或類成員函式。__declspec(dllexport) 在link時會將匯出指令新增到obj檔案中,因此不需要使用 .def 檔案。當然,即使用了__declspec(dllexport)依然可以使用*.def檔案,因為不同編譯器對於類的成員函式的name mangling規則不同,可以定義.def檔案通過序號呼叫。為每個dll寫def顯得很繁雜,目前def使用已經比較少了,更多的是使用__declspec(dllexport)在原始碼中定義dll的輸出函式。

若要輸出類的所有成員:資料or函式,__declspec(dllexport)要放在類名左邊宣告:
class __declspec(dllexport) Class1{}
如果類沒有資料成員,__declspec(dllexport)放在class關鍵字前宣告就會被編譯器忽略,就沒有lib生成,如下:
__declspec(dllexport) class Class1{}

使用 __declspec(dllexport) 的優缺點(zz)
使用 __declspec(dllexport) 非常方便,因為不必考慮維護 .def 檔案和獲取匯出函式的修飾名。例如,如果您設計的 DLL 供自己控制的應用程式使用,則此方法很適用。如果通過新的匯出函式重新生成 DLL,還必須重新生成應用程式,因為如果使用不同版本的編譯器進行重新編譯,則匯出的 C++ 函式的修飾名可能會發生變化。
二、def檔案
其實def檔案的功能相當於extern “C” __declspec(dllexport)
def檔案中PRIVTATE的作用
The optional keyword PRIVATE prevents entryname from being placed in the import library generated by LINK. It has no
effect on the export in the image also generated by LINK.用了PRIVATE,生成的lib裡沒有對應方法或者資料的entryname因此不能被客戶隱式呼叫。

使用 .DEF 檔案的優缺點(zz)
在 .def 檔案中匯出函式使您得以控制匯出序號。當將附加的匯出函式新增到 DLL 時,可以給它們分配更高的序號值(高於任何其他匯出函式)。當您進行此操作時,使用隱式連結的應用程式不必與包含新函式的新匯入庫重新連結。這非常重要,例如,在設計將由許多應用程式使用的第三方DLL 時。可以通過新增附加功能不斷地增強 DLL,同時確保現有應用程式繼續正常使用新的 DLL。MFC DLL 是使用 .def 檔案生成的。

使用 .def 檔案的另一個優點是:可以使用 NONAME 屬性匯出函式,該屬性僅將序號放到 DLL 的匯出表中。對具有大量匯出函式的 DLL,使用NONAME 屬性可以減小 DLL 檔案的大小。有關編寫模組定義語句的資訊,請參見模組定義語句的規則。有關序號匯出的更多資訊,請參見按序號而不是按名稱從 DLL 匯出函式。
使用 .def 檔案的主要缺點是:在 C++ 檔案中匯出函式時,必須將修飾名放到 .def 檔案中,或者通過使用外部“C”用標準 C 連結定義匯出函式,以避免編譯器進行名稱修飾。如果需要將修飾名放到 .def 檔案中,則可以通過使用 DUMPBIN 工具或 /MAP 連結器選項來獲取修飾名。請注意,編譯器產生的修飾名是編譯器特定的。如果將 Visual C++ 編譯器產生的修飾名放到 .def 檔案中,則連結到 DLL 的應用程式必須也是用相同版本的 Visual C++ 生成的,這樣呼叫應用程式中的修飾名才能與
DLL 的 .def 檔案中的匯出名相匹配。 

相關文章