1. 程式人生 > >DLL編寫中extern “C”和__stdcall的作用

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:[email protected]")

(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(別名) = [email protected](要匯出的函式)

或者:

#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:編譯後,函式名被修飾為“[email protected]”。
    2. __cdecl:編譯後,函式名被修飾為“_functionname”。
    3. __fastcall:編譯後,函式名給修飾為“@[email protected]”。
    4. 注:“functionname”為函式名,“number”為引數位元組數。
    5. 注:函式實現和函式定義時如果使用了不同的函式呼叫協議,則無法實現函式呼叫。
  5. C++語言編譯器函式名稱修飾規則
    1. __stdcall:編譯後,函式名被修飾為“[email protected]@YG******@Z”。
    2. __cdecl:編譯後,函式名被修飾為“[email protected]@YA******@Z”。
    3. __fastcall:編譯後,函式名被修飾為“[email protected]@YI******@Z”。
    4. 注:“******”為函式返回值型別和引數型別表。
    5. 注:函式實現和函式定義時如果使用了不同的函式呼叫協議,則無法實現函式呼叫。
    6. C語言和C++語言間如果不進行特殊處理,也無法實現函式的互相呼叫。

--------------------- 本文來自 羊兒葫蘆貝爾 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/dongchongyang/article/details/52926310?utm_source=copy