声明:此篇博客整理于百度百科,加上了一些我的理解
运行环境:win10+vs2015
函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。(百度百科)
几种调用约定:__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal
详细介绍
__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超出了初学者的范围,以后再说吧。