文章目录[隐藏]
电脑中的任何指令都是在CPU上的运行的,但是CPU本身只负责运算不负责存储,数据一般都是存储在内存和寄存器(储存最常用的数据)。
想要理解函数栈帧的创建和销毁,首先必须了解三个知识点:寄存器、常用汇编指令及内存模型。
基础知识介绍
1. 寄存器的种类与功能
寄存器名称 | 功能 |
---|---|
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 对栈进行操作,比如获取函数参数,局部变量等。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。 |
2. 常用汇编指令
- push指令:它首先减少
esp
的值,再将源操作数复制到栈地址,在32位平台上,esp
每次减少4字节。
解释:首先esp
的值减少4字节,再将ebp
的值压入栈中。 - pop指令:它首先把
esp
指向的栈元素内容复制到一个操作数中,再增加esp
的值。在32位平台上,esp
每次增加4字节。
解释:首先将esp
所指地址处的值赋给edi
,再将esp
的值减少4字节。
- mov指令:用于将一个数据从源地址传送到目标地址,源操作地址的内容不变。
解释:将esp
值赋给ebp
,这里并不是将esp
所指向的内存空间的值赋给 ebp
-
sub指令:减操作指令,从寄存器中减去<shifter_operand>表示的数值,并将结果保存到目标寄存器中。
解释:esp-0E4h字节的结果保存在esp中。 -
lea指令:是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数。
解释:将ebp-0E4h的值直接赋给edi,而不是把ebp-0E4h内存地址里的数据赋给eax。 -
rep指令:重复前缀指令,英文缩写 repeat。能够引发其后字符串指令被重复。
-
stos指令:串存储指令,英文缩写 store string。
解释:
上述几条指令通常一起使用,
rep指令重复其上面的指令,ecx
的值是重复的次数,每执行一次,ecx
减 1,直到ecx
减至0。
stos指令将eax
中的值拷贝到es:[edi]
指向的地址。
dword双字 就是四个字节。
ptrpointer缩写 即指针
[ ]里的数据是一个地址值,这个地址指向一个双字型数据
一次拷贝双字(4个字节)的数据到目的地址。
es:[edi]指向目的串
解释:合起来的意思就是,将栈上从ebp-0E4h
开始的位置,向高地址方向的内存赋值 0CCCCCCCCh,重复 39h 次,每次赋值双字(四字节的空间)。 -
call指令:将程序下一条指令的位置的IP压入堆栈中,并转移到调用的子程序。
解释:将下一条指令的IP(00BF1A30)压入栈中,并移动到调用的子程序。 -
jmp指令:无条件跳转指令。
解释:无条件跳转到IP为(0BF3BE0H)的位置。 -
add指令:用于将两个运算子相加,并将结果写入第一个运算子。
解释:给esp
加8,也就是esp
向高地址方向移动 8字节 ,相当于pop
操作后的指针变化。 -
ret指令:用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。
解释:执行这条命令之后,就自动返回刚才call指令的下一行。
3. 内存模型
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
这次演示所使用的环境是windows 10、编译环境 vs2013(debug、Win32)。
在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
友情提示:
不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。
演示函数栈帧的创建销毁过程
首先来看下这次演示使用的代码:
// 为了能够观察全部的细节,所以把代码拆的足够细。
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
按下F10,在视图中打开调用堆栈窗口,我们发现main()
函数被调用了。
但是main()
函数被谁调用了呢?
当我们接着调试到return 0;
之后,再按F10,我们发现程序跳转到了调用main()
函数的函数内
原来main()
函数是被__tmainCRTStartup
函数调用的,而 __tmainCRTStartup
又是被mainCRTStartup
调用的。
接下来分步骤演示函数栈帧的创建和销毁的过程。
1. 为main()函数开辟栈帧
2. 在main()函数中创建变量
3. 调用Add()函数前的准备
4. 为Add()函数开辟栈帧
5. 在Add()函数中创建变量并运算
6. Add()栈帧的销毁
7. 返回main()函数栈帧
可以看到这里返回到了第3步(3. 调用Add()函数前的准备),最后指令call
的下一条指令。
之后的过程还很复杂,这里就不详细展示了。
有兴趣的铁铁们可以自己研究研究。
总结
看到这里,想必以前学习中的许多困惑已经有了答案吧。
比如:
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机值?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用是结束后怎么返回的?
希望可以对大家有所帮助,如果有什么不对的地方,还烦请指教,谢谢!
版权声明:本文为CSDN博主「三•九」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_45691717/article/details/119641497
暂无评论