DLL編寫中extern “C”和__stdcall的作用

動態鏈接庫的使用有兩種方式,一種是顯式調用。一種是隱式調用。

(1)       顯式調用:使用LoadLibrary載入動態鏈接庫、使用GetProcAddress獲取某函數地址。

(2)       隱式調用:可以使用#pragma comment(lib, “XX.lib”)的方式,也可以直接將XX.lib加入到工程中。

 

DLL的編寫

編寫dll時,有個重要的問題需要解決,那就是函數重命名——Name-Mangling。解決方式有兩種,一種是直接在代碼裏解決採用extent”c”、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另一種是採用def文件。

(1)編寫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的規範(林銳的書有這樣說過)。這樣就使得,任何一個支持C語言的編譯器,它編譯出來的obj文件可以共享,鏈接成可執行文件。這是一種標準,如果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,在代碼裏給函數別名。

(2)_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],這樣就不會有多餘的跳轉了。(參考《加密與解密》第三版279頁)

(3)__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")

 

(4)*.def文件的用途

指定導出函數,並告知編譯器不要以修飾後的函數名作爲導出函數名,而以指定的函數名導出函數(比如有函數func,讓編譯器處理後函數名仍爲func)。這樣,就可以避免由於microsoft VC++編譯器的獨特處理方式而引起的鏈接錯誤。

也就是說,使用了def文件,那就不需要extern “C”了,也可以不需要__declspec(dllexport)了(不過,dll的製造者除了提供dll之外,還要提供頭文件,需要在頭文件里加上這extern”C”和調用約定,因爲使用者需要跟製造者遵守同樣的規則,除非使用者和製造者使用的是同樣的編譯器並對調用約定無特殊要求)。

舉例def文件格式:

LIBRARY  XX(dll名稱這個並不是必須的,但必須確保跟生成的dll名稱一樣)

EXPORTS

[函數名] @ [函數序號]

 

編寫好之後加入到VC的項目中,就可以了。

       另外,要注意的是,如果要使用__stdcall,那麼就必須在代碼裏使用上__stdcall,因爲*.def文件只負責修改函數名稱,不負責調用約定。

也就是說,def文件只管函數名,不管函數平衡堆棧的方式。

 

如果把*.def文件加入到工程之後,鏈接的時候並沒有自動把它加進去。那麼可以這樣做:

手動的在link添加:

1)工程的propertiesàConfiguration PropertiesàLinkeràCommand Lineà在“Additional options”里加上:/def:[完整文件名].def

2)工程的propertiesàConfiguration PropertiesàLinkeràInputàModule Definition File里加上[完整文件名].def

 

注意到:即便是使用C的名稱修飾方式,最終產生的函數名稱也可能是會被修飾的。例如,在VC下,_stdcall的調用方式,就會對函數名稱進行修飾,前面加‘_’,後面加上參數相關的其他東西。所以使用*.def文件對函數進行命名很有用,很重要。

 2011-8-14補充

編寫dll可以使用.def文件對導出的函數名進行命名。

 

1、動態裝入dll,重命名(*.def)的必要性?

因爲導出的函數儘可能使用__stdcall的調用方式。而__stdcall的調用方式,無論是C的Name Mangling,還是C++的Name Mangling都會對函數名進行修飾。所以,採用__stdcall調用方式之後,必須使用*.def文件對函數名重命名,不然就不能使用GetProcAddress()通過函數名獲取函數指針。

 

2、隱式調用時,頭文件要注意的地方?

因爲使用靜態裝入,需要有頭文件聲明這個要被使用的dll中的函數,如果聲明中指定了__stdcall或者extern “C”,那麼在調用這個函數的時候,編譯器就通過Name Mangling之後的函數名去.lib中找這個函數,*.def中的內容是對*.lib裏函數的名稱不產生作用,*.def文件裏的函數重命名只對dll有用。這就有lib 跟dll裏函數名不一致的問題了,但並不會產生影響,DLL的製造者跟使用者採用的是一致函數聲明。

 

3、所以到底要不要使用__stdcall 呢?

我看到一些代碼裏是沒有使用__stdcall的。如果不使用__stdcall,而使用默認的調用約定_cdecl,並且有extern ”C”。那麼VC是不會任何修飾的。這樣子生成的dll裏的函數名就是原來的函數名。也就可以不使用.def文件了。

也有一些要求必須使用__stdcall,例如com相關的東西、系統的回調函數。具體看有沒有需要。

4、導出函數別名怎麼寫?

可以在.def文件裏對函數名寫一個別名。

例如:

EXPORTS

cswuygTest(別名) = _showfun@4(要導出的函數)

 

或者:

#pragma comment(linker, "/export:[別名] =[NameMangling後的名稱]")

這樣做就可以隨便修改別名了,不會出現找不到符號的錯誤。

5、用不用*.def文件?

如果採用VC默認的調用約定,可以不用*.def文件,如果要採用__stdcall調用約定,又不想函數名被修飾,那就採用*.def文件吧,另一種在代碼裏寫的重命名的方式不夠方便。

6、什麼情況下(不)需要考慮函數重命名的問題?

1)、隱式調用(通過lib)

如果dll的製造者跟dll的使用者採用同樣的語言、同樣編程環境,那麼就不需要考慮函數重命名。使用者在調用函數的時候,通過Name Mangling後的函數名能在lib裏找到該函數。

如果dll的製造者跟dll使用不同的語言、或者不同的編譯器,那就需要考慮重命名了。

2)、顯示調用(通過GetProcessAddress)

       這絕對是必須考慮函數重命名的。

7、總結

    總的來說,在編寫DLL的時候,寫個頭文件,頭文件裏聲明函數的NameMingling方式、調用約定(主要是爲了隱式調用)。再寫個*.def文件把函數重命名了(主要是爲了顯式調用)。提供*.DLL\*.lib\*.h給dll的使用者,這樣無論是隱式的調用,還是顯式的調用,都可以方便的進行。

 

8.補充:

 

  1. 調用協議常用場合
    1. __stdcall:Windows API默認的函數調用協議。
    2. __cdecl:C/C++默認的函數調用協議。
    3. __fastcall:適用於對性能要求較高的場合。
  2. 函數參數入棧方式
    1. __stdcall:函數參數由右向左入棧。
    2. __cdecl:函數參數由右向左入棧。
    3. __fastcall:從左開始不大於4字節的參數放入CPU的ECX和EDX寄存器,其餘參數從右向左入棧。
    4. 問題一:__fastcall在寄存器中放入不大於4字節的參數,故性能較高,適用於需要高性能的場合。
  3. 棧內數據清除方式
    1. __stdcall:函數調用結束後由被調用函數清除棧內數據。
    2. __cdecl:函數調用結束後由函數調用者清除棧內數據。
    3. __fastcall:函數調用結束後由被調用函數清除棧內數據。
    4. 問題一:不同編譯器設定的棧結構不盡相同,跨開發平臺時由函數調用者清除棧內數據不可行。
    5. 問題二:某些函數的參數是可變的,如printf函數,這樣的函數只能由函數調用者清除棧內數據。
    6. 問題三:由調用者清除棧內數據時,每次調用都包含清除棧內數據的代碼,故可執行文件較大。
  4. C語言編譯器函數名稱修飾規則
    1. __stdcall:編譯後,函數名被修飾爲“_functionname@number”。
    2. __cdecl:編譯後,函數名被修飾爲“_functionname”。
    3. __fastcall:編譯後,函數名給修飾爲“@functionname@nmuber”。
    4. 注:“functionname”爲函數名,“number”爲參數字節數。
    5. 注:函數實現和函數定義時如果使用了不同的函數調用協議,則無法實現函數調用。
  5. C++語言編譯器函數名稱修飾規則
    1. __stdcall:編譯後,函數名被修飾爲“?functionname@@YG******@Z”。
    2. __cdecl:編譯後,函數名被修飾爲“?functionname@@YA******@Z”。
    3. __fastcall:編譯後,函數名被修飾爲“?functionname@@YI******@Z”。
    4. 注:“******”爲函數返回值類型和參數類型表。
    5. 注:函數實現和函數定義時如果使用了不同的函數調用協議,則無法實現函數調用。
    6. C語言和C++語言間如果不進行特殊處理,也無法實現函數的互相調用。

 

 

轉自:http://www.cnblogs.com/cswuyg/archive/2011/09/30/dll.html

            http://blog.sina.com.cn/s/blog_701526f40100lcy6.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章