函數調用約定規定了執行過程當中函數的調用者和被調用者之間如何傳遞參數以及如何恢復棧平衡。數組
在參數傳遞過程當中,有二個問題必須獲得明確說明:編輯器
1 當參數多於1個時,按照什麼順序把參數入棧函數
2 函數調用後 ,由誰把棧恢復原貌this
假設在c語言中,咱們編寫了這麼一個函數:spa
int calculate(int a, int b, int c)操作系統
咱們調用函數calculate時直接傳遞實參就行了。可是,在系統中,CPU執行時確沒有辦法知道一個函數調用須要多少個參數,設計
每一個參數是什麼樣的。就是說計算機不知道怎麼給這個函數傳遞參數,傳遞參數的工做必須由函數調用者和函數自己來協調。3d
怎麼協調呢?指針
函數調用時,函數調用者依次把參數壓棧,而後調用函數,函數調用後,在棧中取得數據,並進行計算。函數調用結束後,blog
或者調用者或者函數自己修改棧,使棧恢復原貌。
在高級語言中,經過函數調用約定來講明參數的入棧和棧的恢復問題。常見的調用約定:
stdcall
cdecl
fastcall
thiscall
naked call
stdcall
stdcall調用約定聲明函數的格式:
int __stdcall func(int x, int y)
stdcall的調用約定規則:
參數入棧規則: 參數從右向左入棧
堆棧平衡:被調用函數自身修改棧
函數名自動加前導的下劃線,後面緊跟一個@符合,其後緊跟着參數的尺寸。
在微軟Windows的C/C++編輯器中,經常使用Pascal宏來聲明這個調用約定,相似的宏還有WINAPI和CALLBACK
cdecl調用約定
cdecl調用約定又稱爲C調用約定,是C語言缺省的調用約定,它聲明函數的格式:
int func(int x, int y) 或 int __cdecl func(int x, int y)均可以
cdecl的調用約定規則:
參數入棧順序: 從右到左
堆棧平衡:調用者修改棧
函數名:前加下劃線
因爲每次函數調用都要由編譯器產生還原棧的代碼,因此使用__cdecl方式編譯的程序比使用__stdcall編譯的程序大不少。
可是__cdecl調用方式是由函數調用者負責清除棧中的函數參數,因此這種方式支持可變參數,好比printf()和Windows API的wsprintf
fastcall調用約定
fastcall調用約定聲明函數的格式:
int fastcall func(int x, int y)
fastcall調用約定規則:
參數入棧順序:函數的第一個和第二個參數經過ecx和edx傳遞,剩餘參數從右到左入棧
堆棧平衡:被調用者修改棧
函數名自動加前導的下劃線,後面緊跟一個@符號,其後緊跟着參數的尺寸
以fastcall聲明執行的函數,具備較快的執行速度,由於函數的前二個參數經過寄存器來傳遞的。
注意,在X64平臺,默認使用了fastcall調用約定,其規則以下:
1 一個函數在調用時,前四個參數是從左至右依次存放於RCX,RDX,R8,R9寄存器裏,剩下的參數從右至左入棧
2 浮點前4個參數傳入XMM0,XMM1,XMM2,XMM3中,其它參數傳遞到堆棧中
3 調用者負責在棧上分配32字節的「shadow space」,用於存放前四個調用參數;
小於64位的參數傳遞時高位並不填充零,大於64位須要按照地址傳遞
4 調用者負責堆棧平衡
5 被調用函數的返回值是整數時,則返回值被存放於RAX;浮點數返回在XMM0中
6 RAX,RCX,RDX,R8,R9,R10,R11是「易揮發」的不用特別保護(保護是指使用前要push備份),
其他寄存器須要保護。(X86下只有eax,ecx,edx是易揮發的)
7 棧須要16字節對齊,call指令會入棧一個8字節的返回值(函數調用前RIP寄存器的值),這樣棧就無法對齊。
因此,全部非葉子結點調用的函數,都必須調整棧RSP的地址位16n+8,來使棧對齊。好比sub rsp, 28h
8 對於R8-R15寄存器,咱們可使用r8,r8d,r8w,r8b分別表明r8寄存器的64位,低32位,低16位和低8位
int __stdcall func1(int x, int y)
{
return x+y;
}
int __cdecl func2(int x, int y)
{
return x+y;
}
int __fastcall func3(int x, int y, int z)
{
return x+y+z;
}
int main(int argc, char* argv[])
{
func1(1, 2);
func2(1, 2);
func3(1, 2, 3);
return 0;
}
對於上面3個函數,分別採起stdcall,cdecl,fastcall3種調用約定,從彙編層來分析參數入棧和棧平衡過程以下:
int __stdcall func1(int x, int y)//採用stdcall
{
42D640 push ebp
0042D641 mov ebp,esp
0042D643 sub esp,0C0h
0042D649 push ebx
0042D64A push esi
0042D64B push edi
0042D64C lea edi,[ebp-0C0h]
0042D652 mov ecx,30h
0042D657 mov eax,0CCCCCCCCh
0042D65C rep stos dword ptr es:[edi]
return x+y;
0042D65E mov eax,dword ptr [x]
0042D661 add eax,dword ptr [y]
}
0042D664 pop edi
0042D665 pop esi
0042D666 pop ebx
0042D667 mov esp,ebp //ebp(調用前的棧頂)放入esp中,而後出棧,恢復老ebp
0042D669 pop ebp
0042D66A ret 8 //被調用者負責棧平衡,ret 8,esp += 8;
int __cdecl func2(int x, int y)//採用cdecl調用約定
{
0042D680 push ebp
0042D681 mov ebp,esp
0042D683 sub esp,0C0h
0042D689 push ebx
0042D68A push esi
0042D68B push edi
0042D68C lea edi,[ebp-0C0h]
0042D692 mov ecx,30h
0042D697 mov eax,0CCCCCCCCh
0040042D69C rep stos dword ptr es:[edi]
return x+y;
0042D69E mov eax,dword ptr [x]
0042D6A1 add eax,dword ptr [y]
}
0042D6A4 pop edi
0042D6A5 pop esi
0042D6A6 pop ebx
0042D6A7 mov esp,ebp
0042D6A9 pop ebp
00000042D6AA ret//被調用者直接返回,不用恢復棧平衡,由調用者負責
int __fastcall func3(int x, int y, int z)//採用fastcall調用約定
{
0042D6C0 push ebp
0042D6C1 mov ebp,esp
0042D6C3 sub esp,0D8h
0042D6C9 push ebx
0042D6CA push esi
0042D6CB push edi
0042D6CC push ecx
0042D6CD lea edi,[ebp-0D8h]
0042D6D3 mov ecx,36h
0042D6D8 mov eax,0CCCCCCCCh
0042D6DD rep stos dword ptr es:[edi]
0042D6DF pop ecx
0042D6E0 mov dword ptr [ebp-14h],edx //前2個參數放在了ecx和edx中
0040042D6E3 mov dword ptr [ebp-8],ecx//前2個參數放在了ecx和edx中
return x+y+z;
0042D6E6 mov eax,dword ptr [x]
0042D6E9 add eax,dword ptr [y]
0042D6EC add eax,dword ptr [z]
}
0042D6EF pop edi
0042D6F0 pop esi
0042D6F1 pop ebx
0042D6F2 mov esp,ebp
0042D6F4 pop ebp
0040042D6F5 ret 4 //第3個參數佔4個字節,從棧上傳遞,因此棧平衡是彈出4個字節
int main(int argc, char* argv[])
{
func1(1, 2); //採用stdcall,參數從右往左依次入棧,被調用者負責棧平衡
//0042D72E push 2 //參數從右往左依次入棧,2入棧
//0042D730 push 1 //參數從右往左依次入棧,1入棧
//0042D732 call func1 (42B6F4h)
func2(1, 2);//採用cdecl調用約定,參數從右往左依次入棧,調用者負責棧平衡
//0042D737 push 2//參數從右往左依次入棧,2入棧
//0042D739 push 1//參數從右往左依次入棧,1入棧
//0042D73B call func2 (42B3FCh)
//0042D740 add esp,8 //調用者負責棧平衡,esp+8,等於2個入棧參數的長度
func3(1, 2, 3);//採用fastcall,前2個參數依次放入ecx和edx寄存器,剩餘參數從右往左依次入棧,被調用者負責棧平衡
//0042D743 push 3 //剩餘參數從右往左依次入棧,3入棧
//0042D745 mov edx,2 //前2個參數,分別送往ecx和edx寄存器,2入edx
//0042D74A mov ecx,1 //前2個參數,分別送往ecx和edx寄存器,1入ecx
//0042D74F call func3 (42B023h)23h)
return 0;
}
x64下的fastcall調用約定:
void __fastcall Func1(int nop1, int nop2, int nop3, int nop4, char arg1, short arg2, int arg3)
{
000000013F1C1020 mov dword ptr [i],r9d
000000013F1C1025 mov dword ptr [rsp+18h],r8d
000000013F1C102A mov dword ptr [rsp+10h],edx
000000013F1C102E mov dword ptr [rsp+8],ecx
000000013F1C1032 push rdi
000000013F1C1033 sub rsp,30h
000000013F1C1037 mov rdi,rsp
000000013F1C103A mov ecx,0Ch
000000013F1C103F mov eax,0CCCCCCCCh
000000013F1C1044 rep stos dword ptr [rdi]
000000013F1C1046 mov ecx,dword ptr [nop1]
int i = 1;
000000013F1C104A mov dword ptr [i],1
printf("hello world\n");
000000013F1C1052 lea rcx,[__xi_z+148h (013F1C68B8h)]
printf("hello world\n");
000000013F1C1059 call qword ptr [__imp_printf (013F1CB228h)]
}
000000013F1C105F add rsp,30h
000000013F1C1063 pop rdi
000000013F1C1064 ret
int main()
{
000000013F1C1070 push rdi
000000013F1C1072 sub rsp,40h
000000013F1C1076 mov rdi,rsp
000000013F1C1079 mov ecx,10h
000000013F1C107E mov eax,0CCCCCCCCh
000000013F1C1083 rep stos dword ptr [rdi]
Func1(0, 0, 0, 0, 1, 200, 3000);//參數前4個進入rcx,rdx,r8,r9寄存器,剩餘的從右往左,依次入棧
000000013F1C1085 mov dword ptr [rsp+30h],0BB8h
000000013F1C108D mov word ptr [rsp+28h],0C8h
000000013F1C1094 mov byte ptr [rsp+20h],1
000000013F1C1099 xor r9d,r9d
000000013F1C109C xor r8d,r8d
000000013F1C109F xor edx,edx
000000013F1C10A1 xor ecx,ecx
000000013F1C10A3 call Func1 (013F1C1005h)
return 0;
000000013F1C10A8 xor eax,eax
}
000000013F1C10AA add rsp,40h
000000013F1C10AE pop rdi
000000013F1C10AF ret
thiscall是C++類成員函數缺省的調用約定,但它沒有顯示的聲明形式。由於在C++類中,成員函數調用還有一個this指針參數,所以必須特殊處理,thiscall意味着:
參數入棧:參數從右向左入棧
this指針入棧:若是參數個數肯定,this指針經過ecx傳遞給被調用者;若是參數個數不肯定,this指針在全部參數壓棧後被壓入棧。
棧恢復:對參數個數不定的,調用者清理棧,不然函數本身清理棧。
這是一個不經常使用的調用約定,編譯器不會給這種函數增長初始化和清理代碼,也不能用return語句返回值,只能用插入彙編返回結果。所以它通常用於實模式驅動程序設計,假設定義減法程序,能夠定義爲:
__declspec(naked) int sub(int a,int b)
{
__asm mov eax,a
__asm sub eax,b
__asm ret
}
上面講解了函數的各類調用約定。那麼若是定義的約定和使用的約定不一致,會出現什麼樣的問題呢?結果就是:則將致使棧被破壞。最多見的調用規約錯誤是:
1. 函數原型聲明和函數體定義不一致
2. DLL導入函數時聲明瞭不一樣的函數約定
下面來研究C語言的活動記錄,即它的棧幀。所謂的活動記錄,就是在程序執行的過程當中函數調用時棧上的內容變化。 一個函數被調用,反映在棧上的與之相關的內容被稱爲一個幀,其中包含了參數,返回地址,老ebp值,局部變量,以及esp,ebp。 下圖就是程序執行時的一個活動記錄。 C語言的默認調用約定爲cdecl。所以C語言的活動記錄中,參數是從右往左依次入棧。以後是函數的返回地址入棧,接着是ebp入棧。
上圖很是重要,建議讀者朋友們必定要對該圖作到成竹在胸。能夠用上圖來分析不少實際問題。好比,能夠用ebp+8取得第一個參數,而後依次取得第二個,第三個,第N個參數。也能夠經過ebp-N來得到棧中的局部變量。
例題:分析下面程序運行狀況,有什麼問題呢?
1 #include
2 void main(void)
3 {
4 char x,y,z;
5 int i;
6 int a[16];
7 for(i=0;i<=16;i++)
8 {
9 a[i]=0;
10 printf("\n");
11 }
12 return 0;
13 }
在分析程序執行時,一個重要的方法就是首先畫出它的活動記錄。根據它的活動記錄,去分析它的執行。對於本題的問題,畫出了下圖的活動記錄。
結合該活動記錄,經過對程序的執行分析,for循環中對數組的訪問溢出了。那麼溢出的後果是什麼呢? 經過上圖的活動記錄,你們能夠看出a[16]實際上對應的是變量i。所以循環的最後一次執行的時候,實際上a[16] = 0 就是將i值從新設爲了0,因而i永遠也不會大於16。所以整個程序中for循環沒法退出,程序陷入死循環。
例題:一個C語言程序以下:
void func(void)
{
char s[4];
strcpy(s, "12345678");
printf("%s\n", s);
}
void main(void)
{
func();
printf("Return from func\n");
}
該程序在X86/Linux操做系統上運行的結果以下:
12345678
Return from func
Segmentation fault(core dumped)
試分析爲何會出現這樣的運行錯誤。
答案:func()函數的活動記錄以下圖所示。在執行字符串拷貝函數以後,因爲」12345678」長度大於4個字節,而strcpy()並不檢查字符串拷貝是否溢出,所以形成s[4]數組溢出。s[4]數組的溢出正好覆蓋了老ebp的內容,可是返回地址並沒被覆蓋。因此程序可以正常返回。但因爲老ebp被覆蓋了,所以從main()函數返回後,出現了段錯誤。所以,形成該錯誤結果的緣由就是func()函數中串拷貝時出現數組越界。