调用约定

声明:此篇博客整理于百度百科,加上了一些我的理解
运行环境:win10+vs2015

函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。(百度百科)

几种调用约定:__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal

  • 参数传递顺序
    1.从右到左依次入栈:__stdcall,__cdecl,__thiscall,__fastcall
    2.从左到右依次入栈:__pascal
  • 调用堆栈清理
    1.调用者清除栈。
    2.被调用函数返回后清除栈。

详细介绍

__cdecl

  1. 参数是从右向左传递的,也是放在堆栈中。
  2. 堆栈平衡是由调用函数来执行的(在call B,之后会有add esp x,x表示参数的字节数)。
  3. 函数重命名是会变成”int __cdecl fun(int,int)” (?fun@@YAHHH@Z)
  4. __cdecl是默认的调用约定

运行一段实例:

int fun(int a, int b) {
    int c = 0;
    c = a + b;
    return 0;
}
int main() {
    int a = 0;
    int b = 0;
    int c = 0;
    c = fun(a, b);
    return 0;
}

在执行c = fun( a, b);时的反汇编代码:

    c = fun(a, b);
00B92413  mov         eax,dword ptr [b]  
00B92416  push        eax  
00B92417  mov         ecx,dword ptr [a]  
00B9241A  push        ecx  
00B9241B  call        fun (0B913FCh)  
    //函数开辟栈帧
        int c = 0;
    000E235E  mov         dword ptr [c],0  
        c = a + b;
    000E2365  mov         eax,dword ptr [a]  
    000E2368  add         eax,dword ptr [b]  
    000E236B  mov         dword ptr [c],eax  
        return 0;
    000E236E  xor         eax,eax  
    }
    00821730  pop         edi  
    00821731  pop         esi  
    00821732  pop         ebx  
    00821733  mov         esp,ebp  
    00821735  pop         ebp  
    00821736  ret  
    //函数返回
00B92420  add         esp,8  
00B92423  mov         dword ptr [c],eax  

首先将参数b的值存入eax,再将参数a的值存入ecx,最后调用函数。函数的返回值由eax进行带回,函数执行完成后esp+8,之后将eax的值传给c。
说明该调用约定是从右边开始进行参数压栈

__stdcall

Win32 API函数绝大部分都是采用__stdcall调用约定的。WINAPI其实也只是__stacall的一个别名而已。(百度百科)

int __stdcall fun(int a, int b) {
    int c = 0;
    c = a + b;
    return 0;
}


int main() {
    int a = 0;
    int b = 0;
    int c = 0;
    c = fun(a, b);
    return 0;
}

还是与上面一样,我们在函数的面前用__stdcall作为修饰符。此时函数将会采用__stdcall调用约定
__stdcall调用约定的主要特征是:
1、参数是从右往左传递的,也是放在堆栈中。
2、函数的堆栈平衡操作是由被调用函数执行的。
3、函数重命名是会变成“int __stdcall fun(int,int)” (?fun@@YGHHH@Z)

    c = fun(a, b);
00BB2413  mov         eax,dword ptr [b]  
00BB2416  push        eax  
00BB2417  mov         ecx,dword ptr [a]  
00BB241A  push        ecx  
00BB241B  call        fun (0BB1401h) 
    // 函数开辟栈帧
        int c = 0;
    00BB235E  mov         dword ptr [c],0  
        c = a + b;
    00BB2365  mov         eax,dword ptr [a]  
    00BB2368  add         eax,dword ptr [b]  
    00BB236B  mov         dword ptr [c],eax  
        return 0;
    00BB236E  xor         eax,eax  
    }
    00BE1730  pop         edi  
    00BE1731  pop         esi  
    00BE1732  pop         ebx  
    00BE1733  mov         esp,ebp  
    00BE1735  pop         ebp  
    00BE1736  ret         8
    //函数返回
00BB2420  mov         dword ptr [c],eax  

由反汇编可知:其反汇编与__cdele大致相同,只是在函数返回时ret了一个8
因为栈的清理(堆栈平衡操作)是由被调用函数执行的。所以使用__stdcall调用约定生成的可执行文件要比__cdecl的要小,因为在每次的函数调用都要产生堆栈清理的代码。函数具有可变参数像printf函数,都必须使用__cdecl调用约定,因为只有调用者才知道参数的数量在每一次的函数调用,因此也只有调用者才能够执行堆栈清理操作。

__fastcall
__fastcall见名知其意,其特点就是快。__fastcall函数调用约定表明了参数应该放在寄存器中,而不是在栈中,调用约定传递参数时,最左边的两个不大于4个字节的参数分别放在ecx和edx寄存器。当寄存器用完的时候,其余参数仍然从右到左的顺序压入堆栈。像浮点值、远指针和__int64类型总是通过堆栈来传递的。
程序源代码:

int __fastcall fun(int a, int b)
{
    int c = 0;
    c = a + b;
    return 0;
}
int main() {
    int a = 0;
    int b = 0;
    int c = 0;
    c = fun(a, b);
    return 0;
}

程序反汇编:

    c = fun(a, b);
00CD1783  mov         edx,dword ptr [b]  
00CD1786  mov         ecx,dword ptr [a]  
00CD1789  call        fun (0CD1357h)
    //函数开辟栈帧  
        int c = 0;
    00CD1726  mov         dword ptr [c],0  
        c = a + b;
    00CD172D  mov         eax,dword ptr [a]  
    00CD1730  add         eax,dword ptr [b]  
    00CD1733  mov         dword ptr [c],eax  
        return 0;
    00CD1736  xor         eax,eax  
    }
    00CD1738  pop         edi  
    00CD1739  pop         esi  
    00CD173A  pop         ebx  
    00CD173B  mov         esp,ebp  
    00CD173D  pop         ebp  
    00CD173E  ret  
    //函数返回
00CD178E  mov         dword ptr [c],eax  

由反汇编可知:在压入参数的时候与__stdcall相比,没有将寄存器压入栈中从而节约了时间。其他的与__stdcall相同。

__thiscall
只能用在非静态成员函数里面
源程序:

class Test {
public:
    int __thiscall fun(int a, int b)
    {
        int c = 0;
        c = a + b;
        return 0;
    }
    int a;
    int b;
};
int main() {
    Test test;
    test.a = 0;
    test.b = 0;
    int c = test.fun(test.a, test.b);
    return 0;
}

反汇编:

    Test test;
    test.a = 0;
01181768  mov         dword ptr [test],0  
    test.b = 0;
0118176F  mov         dword ptr [ebp-0Ch],0  
    int c = test.fun(test.a, test.b);
01181776  mov         eax,dword ptr [ebp-0Ch]  
01181779  push        eax  
0118177A  mov         ecx,dword ptr [test]  
0118177D  push        ecx  
0118177E  lea         ecx,[test]  
01181781  call        Test::fun (0118135Ch)  
            int c = 0;
    01181723  mov         dword ptr [c],0  
            c = a + b;
    0118172A  mov         eax,dword ptr [a]  
    0118172D  add         eax,dword ptr [b]  
    01181730  mov         dword ptr [c],eax  
            return 0;
    01181733  xor         eax,eax  
        }
    01181735  pop         edi  
    01181736  pop         esi  
    01181737  pop         ebx  
    01181738  mov         esp,ebp  
    0118173A  pop         ebp  
    0118173B  ret         8 

01181786  mov         dword ptr [c],eax  

在call指令前将对象的地址存入ecx,在最后函数返回的时候也用了ret 8。

__nakedcall__pascal超出了初学者的范围,以后再说吧。

你可能感兴趣的:(C++,c,调用约定)