函数调用约定以及函数栈帧

栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实 是两个指针寄存器,寄存器ebp为帧指针(指向该栈帧的最底部),而寄存器esp为栈指针(指向该栈帧的最顶部),当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的,换句话说,就是如果该栈存在,ebp帧指针是不移动的,访问栈里面的元素可以以ebp为基准访问ebp指针下面或者上面的元素)。

概括起来就是,栈帧的主要作用是用来控制和保存一个过程的所有信息的。栈帧结构如下所示:


(图片选自互联网)

栈是从高地址向低地址存储。所以越是低的地址,越是靠后入栈。


栈帧对应的汇编代码如下:


下面对一个简单的加法函数调用的程序进行反编译来分析函数栈帧,程序代码如下:

#include <stdio.h>
int Add(int x, int y)
{
 int a = x;
 int b = y;
 return a+b;
}
int main()
{
 int a = 10;
 int b = 20;
 int ret = Add(a, b) ;
 printf("%d\n", ret) ;
 return 0;
}


反编译的代码如下:(本来想放汇编源代码,结果总是排版出问题,无语了)

main()函数如下:





add()函数如下:





栈帧指针绘图分析:

图片转载自http://www.cnblogs.com/33debug/p/6773059.html



开始执行main()函数的状态:



执行了mov ebp,esp之后栈帧指针的变化


可见在19FF40处保留着19FF80,它是初始EBP的值


main()中的赋值语句如下:



执行完赋值语句后ESP的值如下;


ESP的值增加了,而EBP始终作为当前函数操作的基准


add()函数调用参数如下:



因为栈的特性,所以是逆序压入栈,参数入栈后再call add()函数,再看一下堆栈的情况:


确实是逆序的,不过不知道为什么要弄这么多空间


执行完add()的mov ebp,esp之后,栈帧变化如下:


1号箭头指向的是刚压入栈的参数,2号箭头指向的是原EBP的值,因为EBP现在指向被调用函数的栈帧底部,所以基准发生变化,可以参考上一张图片比较


add函数内加法运算,先把传入参数的值赋给eax和ecx,在把eax和ecx的值传给函数内的临时变量local.1和local.2,再相加



add()函数运行完后执行恢复栈帧的语句,执行完后栈帧变化如下:


首先EBP的值变回了原来的值,而且EBP指向最开始的EBP的值,所以在调用函数结束后栈帧会恢复到原来的状态


执行完add()函数后,esp加8,所以add()函数内的局部变量不可用了,因为栈帧无法到达参数所在的地址

接着就是把结果压入栈,然后调用printf()函数,这个过程跟调用add()函数大同小异,所以不展开介绍了


调用完printf()函数后,esp加8,所以函数内的局部变量不可用了,接着esp又加0x4c,栈帧移动,此时整个main()函数内的变量都失效了,所以一开始移动这么多是考虑整个main()可能需要声明使用很多变量


最后main()结束,栈帧恢复到最开始的状态


不过也可以看到,EBP还指向一个19FF94,所以说还是可以继续分析下去的,但是今天就到这里了。


不过这里有个细节,刚才提到了ESP加8,相当于删除被调用函数内的局部变量,但其实函数执行完后,是不用管栈中的参数的,由于只是临时使用存储在栈中的值,下一次在存入其他的值自然会覆盖掉原有值,而且栈内存是固定的,所以既不能也没有必要释放内存。

除此之外,还有个细节,ESP加8是在main()中执行的,其实也是可以在被调用函数中执行,这就是另一个知识点了,函数调用约定里面会对这些加以规定。

函数调用约定(calling convention):对函数调用时如何传递参数的一种约定。

通过前面对函数栈帧的分析,我们知道调用函数前要先把参数压入栈传递给函数。栈内存是固定的,ESP用来指示栈的当前位置,函数调用约定就是解决函数调用后如何处理ESP。主要的函数调用约定有:

  1. cdecl
  2. stdcall
  3. fastcall
一.cdecl

1、采用桟传递参数,参数从右向左依次入栈;

2、由调用者负责恢复栈顶指针;

3、在函数名前加上一个下划线前缀,格式为_function;

要注意的是,调用参数个数可变的函数只能采用这种方式(如printf)。

上面演示的程序就属于cdcel


二.stdcall

1)采用桟传递全部参数,参数从右向左压入栈;
2)
被调用函数负责恢复栈顶指针 ;
3)   函数名自动加前导的下划线,后面是函数名,之后紧跟一个@符号,其后紧跟着参数的尺寸,例如_function@4;




因此,stdcall与cdecl的区别就是谁负责恢复栈顶指针和编译后函数的名称问题。




三.fastcall

采用fasecall的函数声明方式为:
int __fastcall function(int a,int b)
fastcall调用约定意味着:
1、函数的第一个和第二个(从左向右)32字节参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过桟传递。从第三个参数(如果有的话)开始从右向左的顺序压栈;
2、被调用函数恢复栈顶指针;
3、在函数名之前加上"@",在函数名后面也加上“@”和参数字节数,例如@function@8;


这次对函数堆栈及函数调用的学习加深了我对函数的底层认识,也增强了用OD调试的能力


学完之后再看这幅图,真的是感触颇多!!!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值