栈帧
栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实 是两个指针寄存器,寄存器ebp为帧指针(指向该栈帧的最底部),而寄存器esp为栈指针(指向该栈帧的最顶部),当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的,换句话说,就是如果该栈存在,ebp帧指针是不移动的,访问栈里面的元素可以以ebp为基准访问ebp指针下面或者上面的元素)。
概括起来就是,栈帧的主要作用是用来控制和保存一个过程的所有信息的。栈帧结构如下所示:
栈是从高地址向低地址存储。所以越是低的地址,越是靠后入栈。
栈帧对应的汇编代码如下:
下面对一个简单的加法函数调用的程序进行反编译来分析函数栈帧,程序代码如下:
1 | #include <stdio.h> |
反编译的代码如下:
main()函数如下:
1 | push ebp //将ebp压入栈 |
add()函数如下:
1 | push ebp //现将ebp压入栈 |
栈帧指针绘图分析:
开始执行main()函数的状态:
执行了mov ebp,esp之后栈帧指针的变化
可见在19FF40处保留着19FF80,它是初始EBP的值
main()中的赋值语句如下:
1 | mov [local.1],0xA |
执行完赋值语句后ESP的值如下;
ESP的值增加了,而EBP始终作为当前函数操作的基准
add()函数调用参数如下:
1 | mov eax,[local.2] |
因为栈的特性,所以是逆序压入栈,参数入栈后再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()结束,栈帧恢复到最开始的状态
不过这里有个细节,刚才提到了ESP加8,相当于删除被调用函数内的局部变量,但其实函数执行完后,是不用管栈中的参数的,由于只是临时使用存储在栈中的值,下一次在存入其他的值自然会覆盖掉原有值,而且栈内存是固定的,所以既不能也没有必要释放内存。
除此之外,还有个细节,ESP加8是在main()中执行的,其实也是可以在被调用函数中执行,这就是另一个知识点了,函数调用约定里面会对这些加以规定。
函数调用约定(calling convention):对函数调用时如何传递参数的一种约定。
通过前面对函数栈帧的分析,我们知道调用函数前要先把参数压入栈传递给函数。栈内存是固定的,ESP用来指示栈的当前位置,函数调用约定就是解决函数调用后如何处理ESP。主要的函数调用约定有:
cdecl
stdcall
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;
这个图很重要