C++ calling convention

[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

你可能感兴趣的:(C++ calling convention)