什么是栈帧
C/C++程序的基本组成部分是函数。
当程序在运行时,每个函数每次调用都会在调用栈上维护一个独立的栈帧,栈帧中维持着函数所需的各种信息。
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
从逻辑上来说,栈帧就是一个函数的执行环境,它一般包括:
1.函数参数
2.函数的返回地址
3.函数的非静态临时变量
4.编译器自动生成的临时变量
5.函数调用的上下文
......
关于函数调用的上下文
我们都知道,栈是由高地址向低地址生长的。
编译器通常使用寄存器ebp指向当前栈帧的底部(高地址)
使用寄存器esp指向当前栈帧的顶部(低地址)
初识函数调用过程
关于函数调用过程,我们需要知道,既然是调用过程,肯定是有调用者与被调用者
一般来说,
调用者的目的是获取被调用者的返回值,
被调用者则需要知道调用者要给它传入的参数以及它要返回的地址。
在对于具体函数的具体分析上,我们通常还需要详细了解有关函数调用约定的知识,在此不表。
函数调用过程大致可以分为两个部分,共有三步。
两个部分:
函数调用
函数返回
三步:
1.根据被调用函数名找到函数入口
2.在栈中申请用于存储参数及变量内存空间
3.释放在栈中申请的空间,并返回
函数调用
函数调用分为以下几步:
1.参数入栈,将参数按照函数调用约定依次压入栈中
2.返回地址入栈,将当前指令的下一条指令的地址入栈,供函数返回使用
3.进入被调函数,根据函数名找到函数入口,跳转
4.开辟新的栈帧:
ebp压栈
将esp的值赋给ebp
给新栈帧分配空间
函数返回
函数返回分为以下几步:
1.将返回值保存到eax寄存器
2.恢复并回收栈帧空间
3.将栈底恢复到ebp位置
4.ebp出栈,即恢复ebp位置
示例分析
#include <iostream>
using namespace std;
int add(int a, int b) {
int ret = a + b;
return ret;
}
int main() {
int a = 5;
int b = 10;
int r = add(2, 3);
return 0;
}
通过反汇编我们可以查看到程序中每一行代码对应的汇编指令。
push ebp //在栈顶开辟一块空间,用于存放ebp寄存器的值
这里可能会造成理解困难,我们是将edp现在的值压入了栈中,即,将当前栈帧的栈底的地址放入栈顶。这个操作关乎于我们在函数返回时为何可以完整地找回上一个栈帧。
mov ebp,esp //将esp的值存入ebp中,即将ebp指针指向esp指针的指向
sub esp,0E4h //将esp的值减去0E4h,可以看作开辟出了一块0E4h大小的空间
这两步的图示如下:
之后操作是将寄存器ebx。esi,edi压入栈中,并且将sub esp,0E4h开辟的区域全部初始化为0CCCCCCCCh
将函数add的参数压栈,由于C/C++默认使用的函数调用约定为__cdcel,关于这个调用约定,我们此处只需要大概了解以下两点:
1.参数由右到左入栈
2.函数调用结束后由函数调用者清除栈内数据。
所以此时:
通过call指令可以找到add函数入口地址,跳转到add函数处。
这里的操作与main函数入口相仿:
在进行完函数体内部的运算后,到达return语句
add esp,0CCh //sub的反操作,回收我们所开辟的空间
由于栈是由高地址向低地址生长的,所以esp减去一个值,自然是拉大了与ebp的距离,换言之也就是给该栈帧开辟出了一块空间,同理,加上一个值是减小了ebp与esp间的距离,回收了一块空间。
mov esp,ebp //与函数调用时的mov ebp,esp相对应
开辟新栈帧的时候,我们直接将原栈帧的栈顶esp作为了新栈帧的栈底ebp,此处要恢复到原栈帧自然也就只需要将新栈帧的ebp恢复为原栈帧的栈顶即可,反操作。很容易理解。
pop ebp //ebp出栈
本身我们ebp指向的这块地址中保存的就是原栈帧的栈底地址,所以出栈后只需要依葫芦画瓢。
注意:
pop 寄存器a
出栈某个值,将出栈的值写入寄存器a中。
依据pop操作,将ebp中的值改回为原栈帧的栈底。
ret //回复返回地址,压入eip,类似于pop eip
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)