函數(shù)調用約定(Calling Convention),是一個重要的基礎概念,用來規(guī)定調用者和被調用者是如何傳遞參數(shù)的,既調用者如何將參數(shù)按照什么樣的規(guī)范傳遞給被調用者。
在參數(shù)傳遞中,有兩個很重要的問題必須得到明確說明:
I 當參數(shù)個數(shù)多于一個時,按照什么順序把參數(shù)壓入堆棧;
II 函數(shù)調用后,由誰來把堆棧恢復原狀。
假如在C語言中,定義下面這樣一個函數(shù):
int func(int x,int y, int z)
然后傳遞實參給函數(shù)func()就可以使用了。但是,在系統(tǒng)中,函數(shù)調用中參數(shù)的傳遞卻是一門學問。因為在CPU中,計算機沒有辦法知道一個函數(shù)調用需要多少個、什么樣的參數(shù),也沒有硬件可以保存這些參數(shù)。也就是說,計算機不知道怎么給這個函數(shù)傳遞參數(shù),傳遞參數(shù)的工作必須由函數(shù)調用者和函數(shù)本身來協(xié)調。為此,計算機用棧來支持參數(shù)傳遞。
函數(shù)調用時,調用者依次把參數(shù)壓棧,然后調用函數(shù),函數(shù)被調用以后,在堆棧中取得數(shù)據(jù),并進行計算。函數(shù)計算結束以后,或者調用者、或者函數(shù)本身修改堆棧,使堆棧恢復原狀。
在高級語言中,通過函數(shù)調用約定來說明參數(shù)的入棧和堆棧的恢復問題。常見的調用約定有:
__stdcall
__cdecl
__fastcall
thiscall
__naked call
不同的調用規(guī)約,在參數(shù)的入棧順序,堆棧的恢復,函數(shù)名字的命名上就會不同。在編譯后的代碼量,程序執(zhí)行效率上也會受到影響。
在以上幾種調用約定中,只有__cdecl是可以支持變長參數(shù)的(如支持printf()參數(shù)數(shù)量不確定的函數(shù)),由調用者負責堆棧管理,這兩者是相關的,因為調用者負責傳參,知道參數(shù)的數(shù)量和類型,如果要支持數(shù)量不確定的參數(shù)的話,只能是調用者來管理堆棧平衡。__cdecl也是C語言默認的調用約定。__stdcall每次函數(shù)調用都要由編譯器產生還原堆棧的代碼,所以使用__cdecl方式編譯的程序比使用__stdcall方式編譯的程序要大很多。
示例代碼:
#include <stdio.h>
int __stdcall stdcallF(int x, int y); // 被調用函數(shù)自身修改堆棧,WINAPI和CALLBACK
//int cdeclF(int x ,int y); // 默認的C調用約定
int __cdecl cdeclF(int x,int y); // 明確指出C調用約定, 調用者修改堆棧,支持可變參數(shù),比如printf()
int __fastcall fastcallF(int x,int y,int z); // 被調用者修改堆棧
int __declspec(naked) sub(int a,int b) // 編譯器不會給這種函數(shù)增加初始化和清理代碼,
{ // 也不能用return語句返回值,只能程序員控制,
// 插入?yún)R編返回結果。因此它一般用于驅動程序設計
__asm mov eax,a
__asm sub eax,b
__asm ret
}
class CAdd
{
int a,b;
public:
CAdd(int aa,int bb):a(aa),b(bb){}
int add(int c); // 參數(shù)從右向左入棧,this指針最后入棧
// 參數(shù)個數(shù)不確定時,調用者清理堆棧,否則函數(shù)自己清理堆棧
};
int main()
{
int a = 3;
int b = 4;
int c = 5;
int s = 0;
sub(3,4);
s = stdcallF(a,b);
s += cdeclF(a,b);
s += fastcallF(a,b,c);
CAdd cadd(3,4);
s += cadd.add(c);
printf("%dn",s); //38
getchar();
return 0;
}
int __stdcall stdcallF(int x, int y) // WINAPI和CALLBACK
{ // 被調用函數(shù)自身修改堆棧
return x+y;
}
//int cdeclF(int x ,int y) // 默認的C調用約定
int __cdecl cdeclF(int x,int y) // 明確指出C調用約定
{ // 調用者修改堆棧,支持可變參數(shù),比如printf()
return x+y;
}
int __fastcall fastcallF(int x,int y,int z) // 被調用者修改堆棧
{ // 函數(shù)的第一個和第二個參數(shù)通過ecx和edx傳遞,剩余參數(shù)從右到左入棧
// 在X64平臺,默認使用了fastcall調用約定,因其有較多的寄存器
return x+y+z;
}
int CAdd::add(int c) // 參數(shù)從右向左入棧,this指針最后入棧
{ // 參數(shù)個數(shù)不確定時,調用者清理堆棧,否則函數(shù)自己清理堆棧
return a+b+c;
}
我們來看一下__cdecl調用的調用者的匯編:
34: s += cdeclF(a,b);
004010B0 mov edx,dword ptr [ebp-8]
004010B3 push edx // 壓參b
004010B4 mov eax,dword ptr [ebp-4]
004010B7 push eax // 壓參a
004010B8 call @ILT+10(cdeclF) (0040100f)
004010BD add esp,8 // 主調函數(shù)做堆棧平衡,2個int參數(shù),esp偏移8字節(jié)
004010C0 mov ecx,dword ptr [ebp-10h]
004010C3 add ecx,eax
004010C5 mov dword ptr [ebp-10h],ecx // 返回值
__cdecl調用的被函數(shù)的匯編:
49: int __cdecl cdeclF(int x,int y) // 明確指出C調用約定
50: { // 調用者修改堆棧,支持可變參數(shù),比如printf()
00401230 push ebp
00401231 mov ebp,esp
00401233 sub esp,40h
00401236 push ebx
00401237 push esi
00401238 push edi
00401239 lea edi,[ebp-40h]
0040123C mov ecx,10h
00401241 mov eax,0CCCCCCCCh
00401246 rep stos dword ptr [edi]
51: return x+y;
00401248 mov eax,dword ptr [ebp+8]
0040124B add eax,dword ptr [ebp+0Ch]
52: }
0040124E pop edi
0040124F pop esi
00401250 pop ebx
00401251 mov esp,ebp
00401253 pop ebp
00401254 ret // 這里被調函數(shù)沒有做堆棧平衡
__cdecl代表性的棧示意圖:
__cdecl代表性的函數(shù)棧幀的示例代碼:
#include <stdio.h>
struct ST{
int a;
double d;
//ST(int aa,double dd):a(aa),d(dd){};
};
ST test(double d,int a)
{
char ch = 'a';
char chs[5] = "";
ST st;
st.a = a;
st.d = d;
return st;
}
int main()
{
ST s = test(2.3,4);
printf("%d %fn",s.a,s.d);
getchar();
return 0;
}
ref:
http://mallocfree.com/basic/c/c-6-function.htm#79
-End-