引言
在前期学习当中,我们可能会有很多困惑?比如
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用是结束后怎么返回的?
建议大家在观察函数栈帧创建与销毁时,使用的环境不需要太高级的编译器,越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
基础知识
电脑中的任何指令都在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次。
搜索
函数栈帧的创建与销毁
置顶唐唐思于 2023-08-21 14:04:31 发布
阅读量576
点赞数 12
28 篇文章0 订阅
目录
引言
在前期学习当中,我们可能会有很多困惑?比如
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用是结束后怎么返回的?
建议大家在观察函数栈帧创建与销毁时,使用的环境不需要太高级的编译器,越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
基础知识
电脑中的任何指令都在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()函数也是被其他函数调用的。
接下来我们进入正题,函数栈帧的创建与销毁。示例代码:
1. //函数栈帧创建与销毁 2. #include<stdio.h> 3. int Add(int x, int y) 4. { 5. int z = 0; 6. z = x + y; 7. return z; 8. } 9. int main() 10. { 11. int a = 20; 12. int b = 10; 13. int c = 0; 14. c= Add(a, b); 15. printf("%d\n", c); 16. return 0; 17. } 18. //为了细致全方面去观察函数栈帧的创建与销毁,所以把代码拆分的很细 19. //F10调试-----→转到反汇编 20. //转到汇编语言去观察时记得把符号名去掉,更易观察
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】