栈帧
C语言中,每个栈帧对应着一个未运行完的函数,栈中保存了一个函数调用所需要维护的信息,这常常被称为堆栈帧或活动记录。
堆栈帧包括的内容:
- 函数的返回地址和参数
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
- 保存的上下文:包括在函数调用前后需要保持不变的寄存器
我们知道每一次函数调用都是一个过程,一个函数的活动记录用ebp和esp这两个寄存器划定范围
- ebp(帧指针):存放了指向函数栈帧栈底的地址
- esp(栈指针):存放了指向函数栈帧栈顶的地址
函数调用
函数调用步骤:
- 参数入栈:按照函数调用惯例将所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用特定的寄存器传递
- 返回地址入栈: 将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行
- 代码跳转: 跳转到被调用函数的函数体执行
函数体的标准开头:
- push ebp:将调用者的ebp压栈处理,保存指向栈底的ebp的地址,方便函数返回之后的现场恢复
- mov ebp,esp:将当前栈帧切换到新栈帧(将esp值装入ebp,更新栈帧底部), 这时ebp指向栈顶,此时栈顶就是old ebp
- sub esp,XXX:在栈上分配XXX字节的临时空间
- push XXX:如有需要,保存名为XX的寄存器(可保存多个)
函数调用的参数显然存储在函数调用者的栈帧中,不是被调用函数的栈帧中
函数返回
函数返回步骤:
- mov eax, xxx:保存被调用函数的返回值到 eax 寄存器中
- mov esp,ebp:恢复esp,回收局部变量空间
- pop ebp: 将上一个栈帧底部位置恢复到 ebp
- ret:弹出当前栈顶元素,从栈中取到返回地址,并跳转到该位置
gdb反汇编出来的代码主要分为3个部分:
- 指令地址
- 指令相对于当前函数起始地址以字节为单位的偏移
- 指令
举例分析
#include<stdio.h>
int sum(int a,int b)
{
int s=a+b;
return s;
}
int main(int argc,char *argv[])
{
int n=sum(1,2);
printf("n=%d\n",n);
return 0;
}
0x080483db <+0>: push %ebp
需要说明的是:gdb反汇编输出的结果中的指令地址和偏移只是gdb为了让我们更容易阅读代码而附加上去的,保存在内存中的以及被CPU执行的代码只有上图中的指令部分
0x080483db <+0>: push %ebp
0x080483dc <+<