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