栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实 是两个指针寄存器,寄存器ebp为帧指针(指向该栈帧的最底部),而寄存器esp为栈指针(指向该栈帧的最顶部),当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的,换句话说,就是如果该栈存在,ebp帧指针是不移动的,访问栈里面的元素可以以ebp为基准访问ebp指针下面或者上面的元素)。
概括起来就是,栈帧的主要作用是用来控制和保存一个过程的所有信息的。栈帧结构如下所示:
(图片选自互联网)
栈是从高地址向低地址存储。所以越是低的地址,越是靠后入栈。
栈帧对应的汇编代码如下:
下面对一个简单的加法函数调用的程序进行反编译来分析函数栈帧,程序代码如下:
#include
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、采用桟传递参数,参数从右向左依次入栈;
2、由调用者负责恢复栈顶指针;
3、在函数名前加上一个下划线前缀,格式为_function;
要注意的是,调用参数个数可变的函数只能采用这种方式(如printf)。
上面演示的程序就属于cdcel
二.stdcall
1)采用桟传递全部参数,参数从右向左压入栈;
2)被调用函数负责恢复栈顶指针 ;
3) 函数名自动加前导的下划线,后面是函数名,之后紧跟一个@符号,其后紧跟着参数的尺寸,例如_function@4;
因此,stdcall与cdecl的区别就是谁负责恢复栈顶指针和编译后函数的名称问题。
采用fasecall的函数声明方式为:
int __fastcall function(int a,int b)
fastcall调用约定意味着:
1、函数的第一个和第二个(从左向右)32字节参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过桟传递。从第三个参数(如果有的话)开始从右向左的顺序压栈;
2、被调用函数恢复栈顶指针;
3、在函数名之前加上"@",在函数名后面也加上“@”和参数字节数,例如@function@8;
这次对函数堆栈及函数调用的学习加深了我对函数的底层认识,也增强了用OD调试的能力
学完之后再看这幅图,真的是感触颇多!!!