汇编学习第五课之函数调用约定:cdecl,stdcall,fastcall

来源:互联网 发布:防止java接口被刷 编辑:程序博客网 时间:2024/06/03 15:59

1. 函数调用约定介绍

函数调用约定(Calling Convention),是一个重要的基础概念,它规定了程序执行过程中函数的调用者(caller)和被调用这(callee)之间如何传递参数以及如何恢复栈平衡之间的约定。下面就来研究这些函数调用约定。

在参数传递中,有两个很重要的问题必须得到明确说明:

1.当参数个数多于一个时,按照什么顺序把参数压入栈;

2.函数调用后,由谁来把栈恢复原状。

假如在C语言中,定义下面这样一个函数:

int func(int x,int y, int z)

2.计算机硬件如何处理函数调用和函数参数

然后传递实参给函数func()就可以使用了。但是,在系统中,函数调用中参数的传递却是一门学问。因为在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机用栈来支持参数传递。

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改栈,使栈恢复原状。

3.常见函数调用stdcall、cdecl、fastcall介绍

在高级语言中,通过函数调用约定来说明参数的入栈和栈的恢复问题。
常见的调用约定有:
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) //默认的C调用约定

int __cdecl func (int x,int y) //明确指出C调用约定

该调用约定遵循下面的规则:

参数入栈顺序:从右到左

还原栈者:调用者修改栈

函数名:前加下划线:_func

由于每次函数调用都要由编译器产生还原栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是 __cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf()和wsprintf()就是__cdecl调用方式。

由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数了。
fastcall调用约定

fastcall的声明语法为:

int fastcall func (int x,int y)

该调用约定遵循下面的规则:

参数入栈顺序:函数的第一个和第二个参数通过ecx和edx传递,剩余参数从右到左入栈

还原栈者:被调用者修改栈

函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸

以fastcall声明执行的函数,具有较快的执行速度,因为部分参数通过寄存器来进行传递的。

4. 三种函数调用约定实验例子

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;
}

从汇编层来分析参数入栈和栈平衡过程如下:
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;
}

5. 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()函数中串拷贝时出现数组越界。

0 0
原创粉丝点击