内功修炼《函数栈帧的创建和销毁》建议收藏

前言

在前期的学习过程中,我们可能会有很多的困惑:

1️⃣ 局部变量是怎么创建的?

2️⃣ 为什么未初始化的局部变量的值是随机值?

3️⃣ 函数是如何传参的?以及传参的顺序是怎样的?

4️⃣ 形参和实参是什么关系?

5️⃣ 函数调用是怎么做的?

6️⃣ 函数调用结束后是怎么返回的?

⚠ 这里使用的环境是 Visual Studio 2013 ,提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器。

一、 寄存器的概念

寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。
按照功能的不同,可将寄存器分为基本寄存器和移位寄存器两大类。基本寄存器只能并行送入数据,也只能并行输出。移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移,数据既可以并行输入、并行输出,也可以串行输入、串行输出,还可以并行输入、串行输出,或串行输入、并行输出,十分灵活,用途也很广。

二、 通用寄存器的结构

通用寄存器组包括AX、BX、CX、DX4个16位寄存器,用以存放16位数据或地址。也可用作8位寄存器。用作8位寄存器时分别记为AH、AL、BH、BL、CH、CL、DH、DL,只能存放8位数据,不能存放地址

1️⃣ AX(AH、AL):累加器。有些指令约定以AX(或AL)为源或目的寄存器。输入/输出指令必须通过AX或AL实现。

2️⃣ BX(BH、BL):基址寄存器。BX可用作间接寻址的地址寄存器和基地址寄存器,BH、BL可用作8位通用数据寄存器。

3️⃣ CX(CH、CL):计数寄存器。CX在循环和串操作中充当计数器,指令执行后CX内容自动修改,因此称为计数寄存器。

4️⃣ DX(DH、DL):数据寄存器。除用作通用寄存器外,在I/O指令中可用作端口地址寄存器,乘除指令中用作辅助累加器。

三、 指针寄存器和变址寄存器

1️⃣ BP( Base Pointer Register):基址指针寄存器。

2️⃣ SP( Stack Pointer Register):堆栈指针寄存器。

3️⃣ SI( Source Index Register):源变址寄存器。

4️⃣ DI( Destination Index Register):目的变址寄存器。

这组寄存器存放的内容是某一段内地址偏移量,用来形成操作数地址,主要在堆栈操作和变址运算中使用。BP和SP寄存器称为指针寄存器,与SS联用,为访问现行堆栈段提供方便。通常BP寄存器在间接寻址中使用,操作数在堆栈段中,由SS段寄存器与BP组合形成操作数地址即BP中存放现行堆栈段中一个数据区的“基址”的偏移量,所以称BP寄存器为基址指针。
SP寄存器在堆栈操作中使用,PUSH和POP指令是从SP寄存器得到现行堆栈段的段内地址偏移量,所以称SP寄存器为堆栈指针,SP始终指向栈顶。
寄存器SI和DI称为变址寄存器,通常与DS一起使用,为访问现行数据段提供段内地址偏移量。在串指令中,其中源操作数的偏移量存放在SⅠ中,目的操作数的偏移量存放在DI中,SI和DI的作用不能互换,否则传送地址相反。在串指令中,SI、DI均为隐含寻址,此时,SI和DS联用,Dl和ES联用。

四、 EBP和ESP

这是单独把 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);
	return 0;
}

不知道大家有没有好奇 Add 被 main 函数调用,而 main 函数被谁调用呢 ❔❓

1、调试代码后,打开调用堆栈
在这里插入图片描述
2、这时看到调用堆栈这个窗口 main 函数被调用了,问题也就出现了 —— main函数被谁调用了

在这里插入图片描述

3、当程序继续走时,它跳到了crtexe.c文件中,这时再看堆栈窗口发现 main 函数被 __tmainCRTStartup() 调用
在这里插入图片描述
4、而 __tmainCRTStartup() 又被 mainCRTStartup() 调用
在这里插入图片描述


解决完疑问后,这里就细看代码的底层是如何执行的 ❔❓

这里观察C语言代码所对应的汇编代码 —— 调试代码后,右击鼠标转到反汇编
在这里插入图片描述
1️⃣ 先为调用 main 函数的这个函数 __tmainCRTStartup 开辟函数栈帧,并用 esp 和 ebp 维护
在这里插入图片描述
2️⃣ 为 main 函数开辟函数栈帧
在这里插入图片描述
在这里插入图片描述

2.1、push ebp

压栈 ebp,esp 指向的位置也随之改变 (地址减小)

▶ 压栈前
在这里插入图片描述
▶ 压栈后
在这里插入图片描述

📐 验证
在这里插入图片描述

1.2、mov ebp, esp

同 ebp = esp

▶ 赋值前
在这里插入图片描述
▶ 赋值后
在这里插入图片描述

1.3、sub esp, 0E4h

同 esp = esp - 0E4h

▶ 没减前
在这里插入图片描述
▶ 减完后
在这里插入图片描述

1.4、push ebx

压栈后 ebx,esp指向的位置也随之改变 (地址减小)

▶ 压栈前
在这里插入图片描述
▶ 压栈后
在这里插入图片描述
📐 验证
在这里插入图片描述

1.5、push esi

压栈 esi,esp指向的位置也随之改变 (地址减小)

▶ 压栈前
在这里插入图片描述
▶ 压栈后
在这里插入图片描述
📐 验证
在这里插入图片描述

1.6、push edi

▶ 压栈前
在这里插入图片描述
▶ 压栈后
在这里插入图片描述
📐 验证
在这里插入图片描述

1.7、lea edi, [ebp - 0E4h]

load effecitve address,把 ebp - 0E4h 这个地址加载到 edi 里

▶ 加载前
在这里插入图片描述
▶ 加载后
在这里插入图片描述

1.8、mov ecx, 39h
         mov eax, 0cccccccch
         rep stos dword ptr es : [edi]

把 edi 这个位置开始向下的 39h 次 dword 数据全部改为0xcccccccc (word是2个字节,dword是4个字节)

📐 验证
在这里插入图片描述

3️⃣ 初始化 a、b、c 局部变量
在这里插入图片描述
在这里插入图片描述

3.1、mov dword ptr [ebp-8], 0Ah

把 0Ah(10) 放到 ebp-8 的位置

📐 验证
在这里插入图片描述

2.2、mov dword ptr [ebp-14h], 14h

把 14h(20) 放到 ebp-14h(ebp-20) 的位置

📐 验证
在这里插入图片描述

2.3、mov dword ptr [ebp-20h], 0

把 0 放到 ebp-20h(ebp-32) 的位置

📐 验证
在这里插入图片描述

通过以上这里就可以验证未初始化的局部变量是随机值

4️⃣ 调用 Add 函数
在这里插入图片描述
在这里插入图片描述

4.1、mov eax, dword ptr [ebp-14h]

把 ebp-14h 的值 20 放到 eax 里去

4.2、push eax

压栈 eax(20),esp指向的位置也随之改变 (地址减小)

▶ 压栈前
在这里插入图片描述

▶ 压栈后
在这里插入图片描述

4.3、mov ecx, dword ptr [ebp-8]

把 ebp-8 的值 10 放到 ecx 里去

4.4、push ecx

压栈 ecx(10),esp指向的位置也随之改变 (地址减小)

▶ 压栈前
在这里插入图片描述
▶ 压栈后
在这里插入图片描述

4.5、
00C2144B call 00C210E1
00C21450 … (这是下一条指令的地址)

call 指令调用 Add 函数,这里逐语句(F11)执行,发现这里竟然存储着下一条指令的地址,事实上 call 指令把下一条指令的地址压栈了(为了 Add 函数结束后能找回来)

▶ 压栈前
在这里插入图片描述
▶ 压栈前
在这里插入图片描述

4.6、进入 Add 函数前,会先为 Add 函数开辟函数栈帧,这里开辟的方式类似于上面 main,所以这里就不细谈了
在这里插入图片描述

4.7、mov dword ptr [ebp-8], 0

把 0 放到 ebp-8 的位置

4.8、mov eax, dword ptr [ebp+8]

把 ebp+8 的值 10 放到 eax 里

4.9、add eax, dowrd ptr [ebp+0ch]

把 ebp+0ch 的值 20 和 eax 的值 10 相加

4.9.1、mov dowrd ptr [ebp-8], eax

把 eax 的值 30 放到 ebp-8(z) 里去

4.9.2、mov eax, dword ptr [ebp-8]

把 ebp-8 的值 30 放到 eax 里去,这也就是为什么函数结束、局部变量销毁,却能把返回值带回来的原因

4.9.3、
pop edi
pop esi
pop ebx

把栈顶的数据 edi 依次弹出放到 edi 寄存器里去,每一次弹出,esp都向下加一次

▶ pop前
在这里插入图片描述
▶ pop edi
在这里插入图片描述
▶ pop esi
在这里插入图片描述
▶ pop ebx
在这里插入图片描述

4.9.4、mov esp, ebp

同 esp = ebp

▶ 赋值前
在这里插入图片描述
▶ 赋值后
在这里插入图片描述

4.9.5、pop ebp

这里弹出的是 main 函数的栈底,ebp 就找到了 main 函数的栈底,且 esp 往下加了一步

▶ pop前
在这里插入图片描述
▶ pop后
在这里插入图片描述

4.9.5、ret

ret 指令就是让栈顶弹出 call 指令的下一条指令的地址,esp 往下加

▶ ret 前
在这里插入图片描述
▶ ret 后
在这里插入图片描述
4.9.6、add esp, 8

此时 esp 指向的形参,让 esp + 8 后,形参销毁

▶ add 前
在这里插入图片描述
▶ add 后
在这里插入图片描述
4.9.7、mov dword ptr [ebp-20h], eax

把 eax 的值 30 放到 ebp-20h ( c ) 的位置


动画演示:

请添加图片描述

五、总结

1️⃣ 局部变量是怎么创建的?

首先为这个函数分配好栈帧空间,并初始化一部分空间为0xcccccccc,再为局部变量分配空间

2️⃣ 为什么未初始化的局部变量的值是随机值?

在开辟好栈帧空间后,会初始化 0xcccccccc 这样的随机值,而局部变量的初始化操作就会将随机值覆盖

3️⃣ 函数是如何传参的?以及传参的顺序是怎样的?

在调用函数前,会先将函数参数从后向前依次压栈,而进入函数后,它会通过指针的偏移量找到形参

4️⃣ 形参和实参是什么关系?

形参是在压栈时开辟的空间,实参和形参只是值相同,空间是独立的。所以形参是实参的一份临时拷贝,改变形参不会改变实参

5️⃣ 函数调用是怎么做的?

函数调用前,它会记住下一条指令的地址,这样做是为了函数结束后能回的来

6️⃣ 函数调用结束后是怎么返回的?

通过寄存器 eax 返回的,在返回前它会将计算好的值放在 eax 里

版权声明:本文为CSDN博主「跳动的bit」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wh128341/article/details/120232713

生成海报
点赞 0

跳动的bit

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

汇编语言(7)——外中断

CPU除了能够处理指令以外,还应该能够对外部设备进行控制,接收他们的输入,向他们进行输入。 要及时处理外设的输入,CPU需要解决两个问题: 1.外设的输入随时可能发生&#xf

88E1111配置及使用

目录 1 88E1111PHY芯片简介 1.1 Media Interface 1.1.1 Copper 1.1.2 Fiber 1.2 MAC Interface 2 PHY寄存器描述及配置 2.1 R27寄存器Extended

RT-Thread Studio移植LAN8720A驱动

RTT网络协议栈驱动移植(霸天虎) 1、新建工程 ​ 工程路径不含中文路径名,工程名用纯英文不含任何符号。 2、用CubeMx配置板子外设 2.1、配置时钟 ​ 按照自己板子配置相应时钟。