【C语言】你必须知道的栈区细节—函数栈帧(动态详解)

🚩纸上得来终觉浅, 绝知此事要躬行。
🌟主页:June-Frost
🚀专栏:C语言

局部变量为什么是随机值?函数是如何调用的?

✉️ 该篇将使用该编译器,通过介绍栈帧的创建和销毁来深入了解局部变量和函数调用的一些细节。


寄存器

寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。

先了解一下这几个寄存器:

eax:

  • 存储函数调用时返回值的地址
  • 存储线程级别的全局变量、堆栈指针等数据
  • 作为命令和参数传递时的参数值

ebx:

  • 存储基址或偏移量,用于访问数组或结构体中的元素
  • 存储线程级别的全局变量、堆栈指针等数据

ecx:

  • 存储计算结果或临时变量的地址
  • 存储线程级别的全局变量、堆栈指针等数据

edx:

  • 存储动态链接库或共享库的入口地址
  • 存储线程级别的全局变量、堆栈指针等数据

主角:

ebp:

  • 存储函数调用时的临时变量的地址
  • 存储函数调用时返回值的地址(在函数调用前由操作系统维护)
  • 存储栈帧中的基址

esp:

  • 存储当前栈帧的顶部指针,即下一个将要被压入栈中的内存地址
  • 存储函数调用时的临时变量的地址(在函数调用前由编译器维护)
  • 存储调用堆栈的回溯信息

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 = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d\n", c);y
	return 0;
}

在这个代码中,都知道是main函数在调用Add函数
但是main函数谁在调用?
通过调用堆栈来观察:

📘这说明 是mainCRTSTartup() 调用了_tmainCRTSTartup() , _tmainCRTSTartup() 调用了 main(…),main(…)调用了Add(int x, int y)。

那么栈区的大致分布就是:


具体细节

我们将上述代码转到反汇编:

先对这部分代码进行详解:
在将要使用这部分main函数的时候,说明_tmainCRTSTartup() 的栈帧就已经创建完成了。


第一步:

push指令就是将指定的值压入函数的栈中。此处要将ebp的值压栈。
push       ebp

在执行第一步之前是这样的:

执行后为:

可以见到,栈区从高到低使用,esp的值减少了,意味着它确实指向了新压入的空间。

观察内存

说明确实压入的是ebp的值。


第二步:

mov    ebp,esp
将esp的值赋给ebp


第三步:

sub    esp,0E4h
sub指令是减法操作,这里相当于esp-0E4h,0E4h是八进制形式,也就是十进制228,所以为esp-228.

在这步,就成功创造了main函数的栈帧。


第四,五,六步:


这几步都是压栈。

监视:

内存:


第七,八,九,十步:

将这几步显示符号名:

lea指令用于将一个值从内存中复制到寄存器中,它的全称是"Load Effective Address",意思是"加载有效地址"。

这里相当于给edi加载了一个值,那个值其实就是没压栈前的esp的值0x008FFAC0 。

mov      ecx,39h
mov      eax,0CCCCCCCCh
这里都是赋值,将39h赋值给ecx,0CCCCCCCCh赋值给eax。

rep stos      dword ptr es:[edi]

word是2个字节,dword就是double word,是4个字节,这个操作就是将edi之后(包括自己)的39个dword赋为eax的值。
这里最终的效果其实就是将main函数的栈帧空间里的内存全部初始化为cc。


之后三步:

mov指令,将0Ah(10)赋值给[ebp-8]的那块空间,将14h(20)赋值给[ebp-14h]的空间,将0赋值给[ebp-20h]的空间。

初始化后才有确定值,说明如果没有初始化,那么对应的内存空间就是cc cc cc cc, 如果直接使用这种空间,那么对应的值就是随机值。


之后四步

这步是将[ebp-14h]空间的值赋给eax,再将eax压栈,将[ebp-8]空间的值赋给ecx,再将ecx压栈。

这里也反映了一个情况:函数的参数式从右向左传参的。


下一步:

当执行call指令的时候,就是调用函数,会出现两个效果,执行F11,跳转至该界面:

而且在内存上,会储存下一条指令的地址:

效果:


对上述界面再F11,就会进入Add函数。

Add函数内前面的这些汇编,和main有异曲同工之妙。都是在创建栈帧。
所以我将直接用动图直接展示这些步骤:



接下来,就需要找到[ebp - 8]的位置,并赋值0。


然后,找到[ebp + 8] 和 [ebp + 0Ch], 0Ch即12 。 先将[ebp + 8]对应的值赋给eax,再将[ebp + 0Ch]的值加在eax上,这样就完成了两数的相加,之后再将eax的值赋给[ebp - 8]对应的空间。



这里将 [ebp - 8]的值给了eax 。(变量z在出了函数体后,变量就被释放了,为了保存下这个值,编译器将该值放入寄存器中,就可以保证这个值的存在)。


pop用于将栈顶元素弹出到寄存器中 。栈顶原本就是edi的值,弹出后放入edi,这时栈顶就是esi的值,弹出后放入esi, 对于ebx同理。

接下来,将ebp的值给esp。



这两步骤很重要:
这时候栈顶ebp(main)【 这个数据其实就是之前main函数的ebp】

将栈顶数据弹出后再放入ebp其实就找回了这个位置。


ret指令用于将函数的返回地址压入栈中,以便在下一次调用时使用。简单的讲,该处就是会把call指令的下一条指令的地址弹出,并跳转到那里。

这时的栈帧空间就是这样的:


随着Add函数的调用完成,形参就可以释放了。

这里将esp+8后,栈帧空间内就不再管理那两个形参。

然后将eax的值赋值给[ebp - 20h] 。相当于c接收了返回值。

到此为止,其实一个函数栈帧的创建与销毁就完成了。

结语

文章到这里就结束了,本小白才疏学浅,如果文章存在问题,还请大佬们多多指出。💖💖💖

  • 19
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值