目录
引言
基础知识
内存模型
寄存器的种类与功能
常用的汇编指令
函数栈帧创建与销毁
main()函数栈帧的创建
NO1.
NO2.
NO3.
NO4.
NO5.
NO6.
main()函数栈帧变量的创建
调用Add()函数栈帧的预备工作——传参
NO1.
NO2.
NO3.
Add()函数栈帧的创建
Add()函数栈帧变量的创建并运算
NO1.
NO2.
NO3.
NO4.
Add()函数栈帧的销毁
NO1.
NO2.
NO3.
返回main()函数栈帧
问题
NO1.
NO2.
NO3.
引言
在前期学习当中,我们可能会有很多困惑?比如
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用是结束后怎么返回的?
建议大家在观察函数栈帧创建与销毁时,使用的环境不需要太高级的编译器,越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
基础知识
电脑中的任何指令都在CPU上运行,但CPU只负责运算不负责存储。
数据都存储在寄存器,缓存和内存中。
想了解函数栈帧的创建和销毁我们就需要了解到:内存模型,寄存器,常用汇编指令。
那关于寄存器和缓存等之间的关系,会在后面的博文讲解到。
内存模型
这只是一个大致的介绍,后面博文我们也会详细去介绍到内存模型。
寄存器的种类与功能
在我们的函数栈帧创建与销毁。我们重点使用到ESP和EBP。
- ESP(esp):栈指针寄存器(extended stack pointer)。栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。在32位平台上,esp每次减少4个字节。其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。是CPU机制决定的,push,pop等指令会自动调整esp的值。
- EBP(ebp):基址指针,指栈的栈底指针。基址指针寄存器(extended base pointer)。一般与esp配合使用,可以存取某时刻的esp,这个时候就是进入一个函数内后,CPU会将esp的值赋给ebp,此时刻就可以通过ebp对栈进行操作。比如获取函数参数,局部变量等。其内存放一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
- esp和ebp这两个寄存器放置的是地址,这两个地址是用来维护函数栈帧的。每个函数调用,都需要在栈区创建一个空间。当正在调用某函数时,esp和ebp就维护这个函数栈帧的空间。
- 栈区使用:从高地址向低地址消耗/使用。
常用的汇编指令
这里我们只讲解几个我们函数栈帧创建等的汇编指令
函数栈帧创建与销毁
了解了上面的基础知识,我们先大致来看下函数栈帧怎样创建与销毁 。
首先我们有想过一个问题吗?就是main()函数也是函数,那也是被哪个函数调用的吗?
当然。在VS2013中,main()函数也是被其他函数调用的。
接下来我们进入正题,函数栈帧的创建与销毁。示例代码:
//函数栈帧创建与销毁
#include<stdio.h>
int Add(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 20;int b = 10;int c = 0;c= Add(a, b);printf("%d\n", c);return 0;
}
//为了细致全方面去观察函数栈帧的创建与销毁,所以把代码拆分的很细
//F10调试-----→转到反汇编
//转到汇编语言去观察时记得把符号名去掉,更易观察
main()函数栈帧的创建
NO1.
首先我们知道关于此时此时栈区_tmainCRTStartup()函数被调用了,接下来它需要调用main()
PUSH 把字压入堆栈。
先将ebp的值这个空间大小放置到栈区新开辟的栈帧中。
再将esp向上移动到ebp的栈顶的位置。
(esp的值减少4个字节,值减少了ebp这么多的空间大小)
首先esp的值减少4个字节,再将ebp的值压入栈中。
NO2.
MOV 传送字或字节。
将esp的值赋给ebp。这里并不是将esp所指向内存空间的值赋给ebp。
NO3.
SUB 减法.
将esp-0E4H(228)即将esp指针向低地址方向移动0E4H字节。
NO4.
此刻我们发现mian()函数有自己栈区所欲开辟的新空间,那接下来?
- 首先将esp的值减少4个字节,再将ebx的值压入栈中。
- 首先将esp的值减少4个字节,再将esi的值压入栈中。
- 首先将esp的值减少4个字节,再将edi的值压入栈中。
(不确定顺序先后?)
NO5.
LEA(lea):load加载。 load effective address
把[ebp-0E4h]这么多的空间大小放到edi里面去。
NO6.
以上四段汇编代码的意思是:
从edi这个位置向下的39h 这么多的空间大小的双字节dword(4个字节)全部放置为0CCCCCCCCh 这样的内容。
每一次初始化dword,共初始化19h次。
main()函数栈帧变量的创建
MOV 传送字或字节。
把0AH(10)的值赋给 地址为[ebp-8] 的双字节空间
把14h(20)的值赋给 地址为[ebp-14h] 的双字节空间
把0的值赋给 地址为[ebp-20h] 的双字节空间
那如果没有初始化呢?
那么abc的位置就会被初始化为CCCCCCCC随机值。打印abc的时候也就是随机值。
调用Add()函数栈帧的预备工作——传参
NO1.
MOV PUSH
- 将地址为[ebp-14h] 双字节空间大小 的值赋给eax
- 将esp的值减少4个字节,将eax压栈到栈中
NO2.
MOV PUSH
- 将地址为[ebp-8]双字节空间大小 的值赋给ecx
- 将esp的值减少4个字节,将ecx压栈到栈中
NO3.
CALL
- 先esp的值减少4个字节,再将下一条指令的IP(00921A30)压入栈中。
- F11之后,移动到调用的Add()函数的子程序里。
Add()函数栈帧的创建
现在我们正式进入Add函数。首先和main()函数栈帧一样,我们需要在栈区开辟一块新的空间。因为在前面我们详细的讲解了main()函数栈帧的创建,这里大家可以先自己动小脑瓜子想想,画一画过程图,再看最后结果。
- 将ebp的值压入栈中,esp减少4个字节。
- 将esp的值赋给ebp,这里并不是将esp所指向的内存空间的值赋给ebp。
- 将esp-0CCh,即esp向上移动0CCh的空间大小。
- 将ebx压入栈中,esp的值减少4个字节。
- 将esi压入栈中,esp的值减少4个字节。
- 将edi压入栈中,esp的值减少4个字节。
- 从edi向下33h的双字节空间大小全部初始化为0CCCCCCCCh,每一次初始化dword双字节大小,共初始化33h次。
Add()函数栈帧变量的创建并运算
NO1.
MOV
将0的值赋给内存地址为[ebp-8]的双字节空间。
NO2.
MOV
将内存地址[ebp+8] 的双字节空间数据内容 赋给eax。
ADD
将内存地址[ebp+0Ch] 的双字节空间数据内容 加上eax的值 再赋给eax。
NO3.
MOV
将eax寄存器道德数据内容 赋给内存地址为[ebp-8]的双字节空间。
NO4.
MOV
将内存地址为[ebp-8]的双字节空间大小中的数据,赋给eax。
Add()函数栈帧的销毁
NO1.
POP
- 先将esp所指的地址处的值赋给edi,esp值增加4个字节。
- 先将esp所指的地址处的值赋给esi,esp值增加4个字节。
- 先将esp所指的地址处的值赋给ebx,esp值增加4个字节。
NO2.
MOV
将ebp的值赋给esp,这里并不是将ebp所指向的内存空间的值赋给esp
POP
将ebp弹回到原来main()函数栈帧的栈底位置,esp增加4个字节(esp来到ebp的栈顶位置)
NO3.
RET
执行完这条命令,就自动返回刚才call指令的下一条。
返回main()函数栈帧
之后的main()函数栈帧的销毁和Add()函数栈帧的销毁同理,所以我们就不再讲解了。
问题
NO1.
为什么将call指令的下一条指令的地址压入栈帧中?
确保我们调用完Add()函数后,返回main()函数栈帧时能回到call函数的下一条指令执行。
NO2.
为什么将main()函数栈帧的栈底地址ebp压入栈顶?
为了当函数调用返回时,esp和ebp都回到原来维护main()函数栈帧的位置。
NO3.
为什么说形参是实参的一份临时拷贝?
还没有调用Add()函数的时候,已经将参数ab传递过去了,在函数栈帧中已经为ab创建了一块空间,在使用xy的时候,返回这里使用即可。所以我们并没有在Add()函数中为xy创建空间。
✔✔✔✔✔最后,感谢大家的阅读,若有错误和不足,欢迎指正!
接下来的博文会更新一些练习题,到实践中去加深对知识的理解。🙂🙂🙂
代码----------→【gitee:https://gitee.com/TSQXG】
联系----------→ 【邮箱:2784139418@qq.com】