[TOC]
#1. cdecl
cdecl(C Declaration)C调用规范是C/C++语言最常使用的参数传递方法。调用方函数(caller)逆序向被调用方函数(callee)传递参数:以“最后(右侧)的参数、倒数第二......至第一个参数”的顺序传递参数。被调用方函数退出后,应由调用方函数(caller)调整栈指针ESP、将栈恢复成调用其他函数之前的原始状态。
C调用规范用于C/C++语言时,子程序的参数按逆序入栈,因此,在调用如下函数时,先将b入栈,再将a入栈。
int __cdecl AddTwo(int a, int b) {
return a + b;
}
int main()
{
int a = 1, b = 2;
int sum = AddTwo(a, b);
std::cout << "Sum is: " << sum << std::endl;
}
C调用规范用一种简单的方法解决了清除运行时堆栈的问题:程序调用子程序时,在CALL指令的后面紧跟一条语句使堆栈指针(ESP)加上一个数,该数的值为子程序参数所占堆栈空间的总和。下面是调用AddTwo函数的汇编片段。
00B62558 mov dword ptr [a],1
00B6255F mov dword ptr [b],2
int sum = AddTwo(a, b);
00B62566 mov eax,dword ptr [b]
00B62569 push eax ;参数b入栈
00B6256A mov ecx,dword ptr [a]
00B6256D push ecx ;参数a入栈
00B6256E call AddTwo (0B6141Ah)
00B62573 add esp,8 ;从堆栈清除参数
int __cdecl AddTwo(int a, int b) {
00B62390 push ebp
00B62391 mov ebp,esp
00B62393 sub esp,0C0h
00B62399 push ebx
00B6239A push esi
00B6239B push edi
00B6239C lea edi,[ebp-0C0h]
00B623A2 mov ecx,30h
00B623A7 mov eax,0CCCCCCCCh
00B623AC rep stos dword ptr es:[edi]
00B623AE mov ecx,offset _F933C1FA_x64_callingconvertiontest@cpp (0B6F027h)
00B623B3 call @__CheckForDebuggerJustMyCode@4 (0B61285h)
return a + b;
00B623B8 mov eax,dword ptr [a]
00B623BB add eax,dword ptr [b]
}
00B623BE pop edi
00B623BF pop esi
00B623C0 pop ebx
00B623C1 add esp,0C0h
00B623C7 cmp ebp,esp
00B623C9 call __RTC_CheckEsp (0B6128Fh)
00B623CE mov esp,ebp
00B623D0 pop ebp
00B623D1 ret
#2. stdcall
stdcall(Standard Call)与cdecl规范类似,只是有一点不同:被调用方函数在返回之前会执行"RET x"指令还原参数栈,而不会使用单纯的"RET"指令直接返回。这里x的计算方式是:x=参数个数*指针大小(注意:指针的大小在x86结构中的值是4,而在x64是8)。
int __stdcall AddTwo(int a, int b) {
return a + b;
}
int main()
{
int a = 1, b = 2;
int sum = AddTwo(a, b);
std::cout << "Sum is: " << sum << std::endl;
}
stdcall是另一种从堆栈删除参数常用方法。如下所示的AddTwo函数给RET指令添加了一个整数参数,这使得被调用方函数(callee)在返回到调用方函数(caller)时,ESP会加上数值8(这个添加的整数必须与被调用过程参数占用的堆栈空间字节数相等)。
int a = 1, b = 2;
002D2558 mov dword ptr [a],1
002D255F mov dword ptr [b],2
int sum = AddTwo(a, b);
002D2566 mov eax,dword ptr [b]
002D2569 push eax ;参数b入栈
002D256A mov ecx,dword ptr [a]
002D256D push ecx ;参数a入栈
002D256E call AddTwo (02D141Fh)
int __stdcall AddTwo(int a, int b) {
002D2390 push ebp
002D2391 mov ebp,esp
002D2393 sub esp,0C0h
002D2399 push ebx
002D239A push esi
002D239B push edi
002D239C lea edi,[ebp-0C0h]
002D23A2 mov ecx,30h
002D23A7 mov eax,0CCCCCCCCh
002D23AC rep stos dword ptr es:[edi]
002D23AE mov ecx,offset _F933C1FA_x64_callingconvertiontest@cpp (02DF027h)
002D23B3 call @__CheckForDebuggerJustMyCode@4 (02D1285h)
return a + b;
002D23B8 mov eax,dword ptr [a]
002D23BB add eax,dword ptr [b]
}
002D23BE pop edi
002D23BF pop esi
002D23C0 pop ebx
002D23C1 add esp,0C0h
002D23C7 cmp ebp,esp
002D23C9 call __RTC_CheckEsp (02D128Fh)
002D23CE mov esp,ebp
002D23D0 pop ebp
002D23D1 ret 8 ;从堆栈清除参数
stdcall和C相似,参数都是按逆序入栈的。通过在RET指令中添加参数,stdcall不仅减少了子程序调用产生的代码量(减少一条指令),还保证调用程序(caller)永远不会忘记清除堆栈。
#3. fastcall
fastcall优先使用寄存器传递参数,无法通过寄存器传递的参数通过栈传递给被调用方函数(callee)。因为fastcall在内存栈方面的访问压力比较小,所以在早期的CPU平台上遵循fastcall规范的程序会比遵循stdcall和cdecl规范的程序性能更高。但是在现在的、更为复杂的CPU平台上,fastcall规范的性能优势就不那么明显了。
不管是MSVC还是GCC都使用ECX和EDX传递第一个和第二个参数,用栈传递其余的参数。此外,应由被调用方函数(callee)调整栈指针、把参数栈恢复到调用之前的初始状态(这一点与stdcall类似)。
int __fastcall AddThree(int a, int b, int c) {
return a + b + c;
}
int main()
{
int a = 1, b = 2, c = 3;
int sum = AddThree(a, b, c);
std::cout << "Sum is: " << sum << std::endl;
}
如下所示的AddThree函数,参数a和b,分别通过ECX,EDX传递,参数c通过栈传递。被调用方函数(callee)负责清除参数栈。
00A15668 mov dword ptr [a],1
00A1566F mov dword ptr [b],2
00A15676 mov dword ptr [c],3
int sum = AddThree(a, b, c);
00A1567D mov eax,dword ptr [c]
00A15680 push eax ;参数C入栈
00A15681 mov edx,dword ptr [b] ;参数b保存到edx
00A15684 mov ecx,dword ptr [a] ;参数a保存到ecx
00A15687 call AddThree (0A11424h)
int __fastcall AddThree(int a, int b, int c) {
00A12390 push ebp
00A12391 mov ebp,esp
00A12393 sub esp,0D8h
00A12399 push ebx
00A1239A push esi
00A1239B push edi
00A1239C push ecx
00A1239D lea edi,[ebp-0D8h]
00A123A3 mov ecx,36h
00A123A8 mov eax,0CCCCCCCCh
00A123AD rep stos dword ptr es:[edi]
00A123AF pop ecx
00A123B0 mov dword ptr [b],edx
00A123B3 mov dword ptr [a],ecx
00A123B6 mov ecx,offset _F933C1FA_x64_callingconvertiontest@cpp (0A1F027h)
00A123BB call @__CheckForDebuggerJustMyCode@4 (0A11285h)
return a + b + c;
00A123C0 mov eax,dword ptr [a]
00A123C3 add eax,dword ptr [b]
00A123C6 add eax,dword ptr [c]
}
00A123C9 pop edi
00A123CA pop esi
00A123CB pop ebx
00A123CC add esp,0D8h
00A123D2 cmp ebp,esp
00A123D4 call __RTC_CheckEsp (0A1128Fh)
00A123D9 mov esp,ebp
00A123DB pop ebp
00A123DC ret 4 ;从堆栈清除参数
#4. thiscall
thiscall是一种方便C++类成员调用this指针而特别设定的调用规范。MSVC使用ECX寄存器传递this指针,而GCC则把this指针作为被调用方函数的第一个参数传递。函数参数通过栈传递,被调用方函数(callee)负责清理参数栈(与stdcall一样)。
class Test {
public:
int __thiscall Add(int a, int b, int c) {
return a + b + c;
}
};
int main()
{
int a = 1, b = 2, c = 3;
Test test;
int sum = test.Add(a, b, c);
std::cout << "Sum is: " << sum << std::endl;
}
如下所示的Add函数调用,函数参数逆序入栈,this指针保存在ECX寄存器中。被调用方函数(callee)Add通过ret 0Ch清理参数栈。
001B5D62 mov dword ptr [a],1
001B5D69 mov dword ptr [b],2
001B5D70 mov dword ptr [c],3
Test test;
int sum = test.Add(a, b, c);
001B5D77 mov eax,dword ptr [c]
001B5D7A push eax ;参数C入栈
001B5D7B mov ecx,dword ptr [b]
001B5D7E push ecx ;参数b入栈
001B5D7F mov edx,dword ptr [a]
001B5D82 push edx ;参数a入栈
001B5D83 lea ecx,[test] ;this指针传递给ECX
001B5D86 call Test::Add (01B1429h)
class Test {
public:
int __thiscall Add(int a, int b, int c) {
001B2390 push ebp
001B2391 mov ebp,esp
001B2393 sub esp,0CCh
001B2399 push ebx
001B239A push esi
001B239B push edi
001B239C push ecx
001B239D lea edi,[ebp-0CCh]
001B23A3 mov ecx,33h
001B23A8 mov eax,0CCCCCCCCh
001B23AD rep stos dword ptr es:[edi]
001B23AF pop ecx
001B23B0 mov dword ptr [this],ecx
001B23B3 mov ecx,offset _F933C1FA_x64_callingconvertiontest@cpp (01BF027h)
001B23B8 call @__CheckForDebuggerJustMyCode@4 (01B1285h)
return a + b + c;
001B23BD mov eax,dword ptr [a]
001B23C0 add eax,dword ptr [b]
001B23C3 add eax,dword ptr [c]
}
001B23C6 pop edi
001B23C7 pop esi
001B23C8 pop ebx
001B23C9 add esp,0CCh
001B23CF cmp ebp,esp
001B23D1 call __RTC_CheckEsp (01B128Fh)
001B23D6 mov esp,ebp
001B23D8 pop ebp
001B23D9 ret 0Ch ;从堆栈清除参数
#5. Summarize
Keyword | Stack cleanup | Parameter passing |
---|---|---|
__cdecl | Caller | Pushes parameters on the stack, in reverse order (right to left) |
__stdcall | Callee | Pushes parameters on the stack, in reverse order (right to left) |
__fastcall | Callee | Stored in registers, then pushed on stack |
__thiscall | Callee | Pushed on stack; this pointer stored in ECX |