_cdecl和__stdcall都是函數調用規範(還有一個__fastcall),規定了參數出入棧的 順序和方法,若是隻用VC編程的話能夠不用關心,可是要在C++和Pascal等其餘語言通訊的時候就要注意了,只有用相同的方法纔可以調用成功.另外, 像printf這樣接受可變個數參數的函數只有用cdecl纔可以實現.
__declspec主要是用於說明DLL的引出函數的,在某些狀況下用__declspec(dllexport)在DLL中生命引出函數,比用傳統的 DEF文件方便一些.在普通程序中也能夠用__declspec(dllimport)說明函數是位於另外一個DLL中的導出函數.
int WINAPI MessageBoxA(HWND,LPCSTR,LPSTR,UINT);
而WINAPI實際上就是__stdcall.
大多數API都採用__stdcall調用規範,這是由於幾乎全部的語言都支持__stdcall調用.相比之下,__cdecl只有在C語言中才能用. 可是__cdecl調用有一個特色,就是可以實現可變參數的函數調用,好比printf,這用__stdcall調用是不可能的.
__fastcall這種調用規範比較少見,可是在Borland C++ Builder中比較多的採用了這種調用方式.
若是有共享代碼的須要,好比寫DLL,推薦的方法是用__stdcall調用,由於這樣適用範圍最廣.若是是C++語言寫的代碼供Delphi這樣的語言 調用就必須聲明爲__stdcall,由於Pascal不支持cdecl調用(或許Delphi的最新版本可以支持也說不定,這個我不太清楚).在其餘一 些地方,好比寫COM組件,幾乎都用的是stdcall調用.在VC或Delphi或C++Builder裏面均可以從項目設置中更改默認的函數調用規 範,固然你也能夠在函數聲明的時候加入__stdcall,__cdecl,__fastcall關鍵字來明確的指示本函數用哪一種調用規範.
__declspec通常都是用來聲明DLL中的導出函數.這個關鍵字也有一些其餘的用法,不過很是罕見.html
DLL中調用約定和名稱修飾
調用約定(Calling Convention)是指在程序設計語言中爲了實現函數調用而創建的一種協議。這種協議規定了該語言的函數中的參數傳送方式、參數是否可變和由誰來處理堆棧等問題。不一樣的語言定義了不一樣的調用約定。
在C++中,爲了容許操做符重載和函數重載,C++編譯器每每按照某種規則改寫每個入口點的符號名,以便 容許同一個名字(具備不一樣的參數類型或者是不一樣的做用域)有多個用法,而不會打破現有的基於C的連接器。這項技術一般被稱爲名稱改編(Name Mangling)或者名稱修飾(Name Decoration)。許多C++編譯器廠商選擇了本身的名稱修飾方案。
所以,爲了使其它語言編寫的模塊(如Visual Basic應用程序、Pascal或Fortran的應用程序等)能夠調用C/C++編寫的DLL的函數,必須使用正確的調用約定來導出函數,而且不要讓編譯器對要導出的函數進行任何名稱修飾。
1.調用約定(Calling Convention)
調用約定用來處理決定函數參數傳送時入棧和出棧的順序(由調用者仍是被調用者把參數彈出棧),以及編譯器用來識別函數名稱的名稱修飾約定等問題。在Microsoft VC++ 6.0中定義了下面幾種調用約定,咱們將結合彙編語言來一一分析它們:
一、__cdecl
__cdecl是C/C++和MFC程序默認使用的調用約定,也能夠在函數聲明時加上__cdecl關鍵字 來手工指定。採用__cdecl約定時,函數參數按照從右到左的順序入棧,而且由調用函數者把參數彈出棧以清理堆棧。所以,實現可變參數的函數只能使用該 調用約定。因爲每個使用__cdecl約定的函數都要包含清理堆棧的代碼,因此產生的可執行文件大小會比較大。__cdecl能夠寫成_cdecl。
下面將經過一個具體實例來分析__cdecl約定:
在VC++中新建一個Win32 Console工程,命名爲cdecl。其代碼以下:
int __cdecl Add(int a, int b); //函數聲明
void main()
{
Add(1,2); //函數調用
}
int __cdecl Add(int a, int b) //函數實現
{
return (a + b);
}
函數調用處反彙編代碼以下:
;Add(1,2);
push 2 ;參數從右到左入棧,先壓入2
push 1 ;壓入1
call @ILT+0(Add) (00401005) ;調用函數實現
add esp,8 ;由函數調用清棧
二、__stdcall
__stdcall調用約定用於調用Win32 API函數。採用__stdcal約定時,函數參數按照從右到左的順序入棧,被調用的函數在返回前清理傳送參數的棧,函數參數個數固定。因爲函數體自己知 道傳進來的參數個數,所以被調用的函數能夠在返回前用一條ret n指令直接清理傳遞參數的堆棧。__stdcall能夠寫成_stdcall。
仍是那個例子,將__cdecl約定換成__stdcall:
int __stdcall Add(int a, int b)
{
return (a + b);
}
函數調用處反彙編代碼:
; Add(1,2);
push 2 ;參數從右到左入棧,先壓入2
push 1 ;壓入1
call @ILT+10(Add) (0040100f) ;調用函數實現
函數實現部分的反彙編代碼:
;int __stdcall Add(int a, int b)
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
;return (a + b);
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;清棧
三、__fastcall
__fastcall約定用於對性能要求很是高的場合。__fastcall約定將函數的從左邊開始的兩個 大小不大於4個字節(DWORD)的參數分別放在ECX和EDX寄存器,其他的參數仍舊自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的堆棧。 __fastcall能夠寫成_fastcall。
依舊是相相似的例子,此時函數調用約定爲__fastcall,函數參數個數增長2個:
int __fastcall Add(int a, double b, int c, int d)
{
return (a + b + c + d);
}
函數調用部分的彙編代碼:
;Add(1, 2, 3, 4);
push 4 ;後兩個參數從右到左入棧,先壓入4
mov edx,3 ;將int類型的3放入edx
push 40000000h ;壓入double類型的2
push 0
mov ecx,1 ;將int類型的1放入ecx
call @ILT+0(Add) (00401005) ;調用函數實現
函數實現部分的反彙編代碼:
; int __fastcall Add(int a, double b, int c, int d)
push ebp
mov ebp,esp
sub esp,48h
push ebx
push esi
push edi
push ecx
lea edi,[ebp-48h]
mov ecx,12h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-8],edx
mov dword ptr [ebp-4],ecx
;return (a + b + c + d);
fild dword ptr [ebp-4]
fadd qword ptr [ebp+8]
fiadd dword ptr [ebp-8]
fiadd dword ptr [ebp+10h]
call __ftol (004011b8)
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 0Ch ;清棧
關鍵字__cdecl、__stdcall和__fastcall能夠直接加在要輸出的函數前,也能夠在編 譯環境的Setting...->C/C++->Code Generation項選擇。它們對應的命令行參數分別爲/Gd、/Gz和/Gr。缺省狀態爲/Gd,即__cdecl。當加在輸出函數前的關鍵字與編譯 環境中的選擇不一樣時,直接加在輸出函數前的關鍵字有效。
四、thiscall
thiscall調用約定是C++中的非靜態類成員函數的默認調用約定。thiscall只能被編譯器使 用,沒有相應的關鍵字,所以不能被程序員指定。採用thiscall約定時,函數參數按照從右到左的順序入棧,被調用的函數在返回前清理傳送參數的棧,只 是另外經過ECX寄存器傳送一個額外的參數:this指針。
此次的例子中將定義一個類,並在類中定義一個成員函數,代碼以下:
class CSum
{
public:
int Add(int a, int b)
{
return (a + b);
}
};
void main()
{
CSum sum;
sum.Add(1, 2);
}
函數調用部分彙編代碼:
;CSum sum;
;sum.Add(1, 2);
push 2 ;參數從右到左入棧,先壓入2
push 1 ;壓入1
lea ecx,[ebp-4] ;ecx存放了this指針
call @ILT+5(CSum::Add) (0040100a) ;調用函數實現
函數實現部分彙編代碼:
;int Add(int a, int b)
push ebp
mov ebp,esp
sub esp,44h ;多用了一個4bytes的空間用於存放this指針
push ebx
push esi
push edi
push ecx
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-4],ecx
;return (a + b);
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;清棧
五、naked屬性
採用上面所述的四種調用約定的函數在進入函數時,編譯器會產生代碼來保存ESI、EDI、EBX、EBP寄 存器中的值,退出函數時則產生代碼恢復這些寄存器的內容。對於定義了naked屬性的函數,編譯器不會自動產生這樣的代碼,須要你手工使用內嵌彙編來控制 函數實現中的堆棧管理。因爲naked屬性並非類型修飾符,故必須和__declspec共同使用。下面的這段代碼定義了一個使用了naked屬性的函 數及其實現:
__declspec ( naked ) func()
{
int i;
int j;
_asm
{
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
}
_asm
{
mov esp, ebp
pop ebp
ret
}
}
naked屬性與本節關係不大,具體請參考MSDN。
六、WINAPI
還有一個值得一提的是WINAPI宏,它能夠被翻譯成適當的調用約定以供函數使用。該宏定義於windef.h之中。下面是在windef.h中的部份內容:
#define CDECL _cdecl
#define WINAPI CDECL
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define APIENTRY WINAPI
因而可知,WINAPI、CALLBACK、APIENTRY
等宏的做用。
2.名稱修飾(Name Decoration)
C或C++函數在內部(編譯和連接)經過修飾名(Decoration Name)識別。函數的修飾名是編譯器在編譯函數定義或者原型時生成的字符串。編譯器在建立.obj文件時對函數名稱進行修飾。有些狀況下使用函數的修飾 名是必要的,如在模塊定義文件裏頭指定輸出C++重載函數、構造函數、析構函數,又如在彙編代碼裏調用C或C++函數等。
在VC++中,函數修飾名由編譯類型(C或C++)、函數名、類名、調用約定、返回類型、參數等多種因素共同決定。下面分C編譯、C++編譯(非類成員函數)和C++類及其成員函數編譯三種狀況說明:
一、C編譯時函數名稱修飾
當函數使用__cdecl調用約定時,編譯器僅在原函數名前加上一個下劃線前綴,格式爲_functionname。例如:函數int __cdecl Add(int a, int b),輸出後爲:_Add。
當函數使用__stdcall調用約定時,編譯器在原函數名前加上一個下劃線前綴,後面加上一個@符號和函數參數的字節數,格式爲
_functionname@number。例如:函數int __stdcall Add(int a, int b),輸出後爲:
_Add@8。
當函數是用__fastcall調用約定時,編譯器在原函數名前加上一個@符號,後面是加一個@符號和函數 參數的字節數,格式爲@functionname@number。例如:函數int __fastcall Add(int a, int b),輸出後爲:@Add@8。
以上改變均不會改變原函數名中的字符大小寫。