概述
一个过程调用包括把数据和控制从代码的一部分转移到另一部分。同时,过程还需要为局部变量分配空间,在执行完该部分后退出时释放这些空间。其中,数据传递、局部变量的空间分配和释放通过操作程序栈来实现。
栈帧结构
既然需要操作栈,我们先来看看栈的结构。栈帧是为单个过程分配的部分。
栈帧的大小由栈顶指针%esp和栈底指针%ebp确定。
1、发生调用时:当使用call语句调用某个函数时,通常分为两步操作:1)将返回地址压入栈中(即在调用者中执行完调用函数后的下一句的地址)2)将下一条要执行的指令的地址赋为被调用函数的地址(%eip = 该地址)。
2、被调用函数结束时:通常汇编代码中使用ret语句返回到call指令后的指令。leave语句是为返回准备栈:1)先释放空间。2)恢复调用者的%ebp。
//每个函数最后都有这两句
leave //等价于movl %edp,%esp;这句指令将栈顶指针和栈底指针重合,此时分配的空间为0,释放了空间。然后恢复调用者的ebp,pop %ebp,将当前地址的值(调用者的%ebp)弹出栈,并赋给%ebp,此时%ebp已恢复原来位置。
ret //将下一条执行的指令地址赋为call指令的后一条的地址
接下来以调用者和被调用者的角度理解:
调用者:
1)准备要传递的参数,入栈的顺序为从右到左。
2)保存返回地址,压入栈。
3)将被调用函数的地址---->%eip。
被调用者:
1)保存调用者的%ebp,入栈。
2)开辟该函数空间
3)执行完后释放空间
4)恢复%ebp、%esp。
5)返回地址---->%eip。
例子讲解
C语言代码:
#include<stdio.h>
int sum (int x ,int y){
int z = 0;
z= x+y;
return z;
}
void main(){
int a= 3,b=4;
int c = sum(a,b); //这里参数入栈顺序是b--->a
printf("the sum is %d\n",c);
}
Ubantu下查看汇编代码:
main函数:
前4行是做准备,开辟空间。由第4行可知main函数的空间大小为20个字节。
接下来准备本地变量a,b,入栈保存。执行赋值后的结果。a,b分别放在esp+14,esp+18处。
接下来调用sum函数
先看看ebp,esp的变化。
为啥ebp是这个值呢?可以看到main函数准备好参数之后,esp的值为0xbffff214。然后存入返回地址、旧的ebp,所以ebp的值就等于0xbffff214-12 = 0xbffff1f8(地址、int型整数、还有参数的大小都是4个字节),也可以得出调用者和被调用者的在栈内的空间是相连的。
打印edp,发现存的是旧的ebp的值0xbffff228。
z=x+y的执行情况,先取x、y的值,再进行相加,最后再赋给z。我们打印z的值是7,同时z是保存在ebp-4处,所以查看ebp-4处的值也是7.
最后返回到main函数。此时的ebp,esp已经恢复到原来的值。并且eip的值将是call指令的下一条指令的地址。下面进行验证:
可见call sum指令的下一条的地址为0x0804842e,与eip值相等。
之后程序将执行完main后面的语句,单个过程的调用到此结束。
- 后面的递归调用、嵌套调用实现基本一致,如果有机会,后面再详细讲解。本篇博客为本人复习所用,刚刚接触,难免有错误,希望大家多多指教。