函数调用约定
函数调用约定规定了执行过程中函数的调用者和被调用者之间如何传递参数以及如何恢复栈平衡。
在参数传递过程中,有二个问题必须得到明确说明:
1 当参数多于1个时,按照什么顺序把参数入栈
2 函数调用后 ,由谁把栈恢复原貌
假设在c语言中,我们编写了这么一个函数:
int calculate(int a, int b, int c)
我们调用函数calculate时直接传递实参就好了。但是,在系统中,CPU执行时确没有办法知道一个函数调用需要多少个参数,
每个参数是什么样的。就是说计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。
怎么协调呢?
函数调用时,函数调用者依次把参数压栈,然后调用函数,函数调用后,在栈中取得数据,并进行计算。函数调用结束后,
或者调用者或者函数本身修改栈,使栈恢复原貌。
在高级语言中,通过函数调用约定来说明参数的入栈和栈的恢复问题。常见的调用约定:
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调用约定
thiscall是C++类成员函数缺省的调用约定,但它没有显示的声明形式。因为在C++类中,成员函数调用还有一个this指针参数,因此必须特殊处理,thiscall意味着:
参数入栈:参数从右向左入栈
this指针入栈:如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入栈。
栈恢复:对参数个数不定的,调用者清理栈,否则函数自己清理栈。
naked call 调用约定
这是一个不常用的调用约定,编译器不会给这种函数增加初始化和清理代码,也不能用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()函数中串拷贝时出现数组越界。