文章目录
- 前言
- 函数栈帧
- 相关寄存器
- 相关汇编指令
- 内存
- 函数栈帧的创建销毁过程
前言
为了更好的了解函数里面变量是如何创建,为什么创建的变量是随机值和函数怎么传参和顺序是怎样的、以及实参和形参的关系,还要函数之间的调用、返回和销毁的过程。我们今天就好好了解一下这个函数栈帧的创建和销毁。
注意:这里使用的环境是vs2013,编译器越高越不好观察细节。
函数栈帧
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,为了了解这个过程我们还要了解相关寄存器和相关的汇编指令以及内存。
相关寄存器
寄存器名称 | 功能 |
---|---|
eax | 累加寄存器,相对于其他寄存器,在运算方面比较常用。保留临时数据,常用于返回值。 |
ebx | 基地址寄存器,在内存寻址时存放基地址。保留临时数据 |
ecx | 计数寄存器,用于循环操作,比如重复的字符存储操作,或者数字统计。 |
edx | 作为EAX的溢出寄存器,总是被用来放整数除法产生的余数。 |
esi | 源变址(索引)寄存器,主要用于存放存储单元在段内的偏移量。通常在内存操作指令中作为“源地址指针”使用。 |
edi | 目的变址(索引)寄存器,主要用于存放存储单元在段内的偏移量。 |
eip | 控制寄存器,存储CPU下次所执行的指令地址(存放指令偏移地址)。保存当前指令的下一条指令的地址。 |
esp | 栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。在32位平台上,esp每次减少4字节。栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。是CPU机制决定的,push、pop指令会自动调整esp的值。 |
ebp | 基址指针,指栈的栈底指针。基址指针寄存器(extended base pointer),一般与esp配合使用,可以存取某时刻的esp,这个时刻就是进入一个函数内后,CPU会将esp的值赋给ebp,此时就可以通过ebp对栈进行操作,比如获取函数参数,局部变量等。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。 |
相关汇编指令
push指令:它首先减少esp的值,再将源操作数复制到栈地址,在32位平台上,esp每次减少4字节。
pop指令:它首先把esp指向的栈元素内容复制到一个操作数中,再增加esp的值。在32位平台上,esp每次增加4字节。
mov指令:用于将一个数据从源地址传送到目标地址,源操作地址的内容不变。
sub指令:减操作指令,从寄存器中减去<shifter_operand>表示的数值,并将结果保存到目标寄存器中。
lea指令:是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数。
rep指令:重复前缀指令,英文缩写 repeat。能够引发其后字符串指令被重复。
stos指令:串存储指令,英文缩写 store string。
call指令:将程序下一条指令的位置的IP压入堆栈中,并转移到调用的子程序。
jmp指令:无条件跳转指令。
add指令:用于将两个运算子相加,并将结果写入第一个运算子。
ret指令:用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。
内存
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:后进先出(Last In First Out),简称LIFO结构。就像叠成一叠的书,先叠上去的书在最下面,因此要最后才能取出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧。
这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
函数栈帧的创建销毁过程
以下面代码为例:
#include <stdio.h>int add(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 10;int b = 30;int c = 0;c = add(a, b);printf("%d\n", c);return 0;
}
按F10开始调试,然后调试–>窗口–>调用堆栈,然后点击那个显示外部代码,一直按F10,函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由 __tmainCRTStartup函数来调用main函数。而又是被mainCRTStartup调用的。。那我们可以确定, __tmainCRTStartup 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈帧,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。那我们就从main函数的栈帧创建。
调试到main函数开始执行的第一行,右击鼠标转到反汇编。
下面的操作和上面差不多,但是返回的时候要注意啦