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

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

文章目录

前言

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

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 里


相关文章
|
C语言
数据结构之栈详解(C语言手撕)
数据结构之栈详解(C语言手撕)
403 1
|
存储 编译器 C语言
【深入理解函数栈帧:探索函数调用的内部机制】
【深入理解函数栈帧:探索函数调用的内部机制】
277 0
|
缓存
银河麒麟server-V10配置镜像源
银河麒麟server-V10配置镜像源
14340 0
|
定位技术
干货!解决Cesium中Entity移动漂移的问题
案例场景:在Cesium开发三维场景展示中,肯定会碰到加载Entity的需求,如果在你的gis应用中,带了地形的展示。那么在旋转切换画面时,Entity是否跟着一起动了起来,感觉像漂移一样呢?
4077 0
干货!解决Cesium中Entity移动漂移的问题
|
缓存 Linux 开发工具
CentOS 7- 配置阿里镜像源
阿里镜像官方地址http://mirrors.aliyun.com/ 1、点击官方提供的相应系统的帮助 :2、查看不同版本的系统操作: 下载源1、安装wget yum install -y wget2、下载CentOS 7的repo文件wget -O /etc/yum.
253941 0
|
JavaScript 前端开发 jenkins
【前端】vue项目打包Browserslist: caniuse-lite is outdated. Please run: npx update-browserslist-db@latest解决方案
【前端】vue项目打包Browserslist: caniuse-lite is outdated. Please run: npx update-browserslist-db@latest解决方案
2787 0
|
10月前
|
缓存 API 开发工具
有关Unity使用Rider编辑器无法弹出代码提示的有效解决方法
【11月更文挑战第13天】在 Unity 中使用 Rider 编辑器时,若遇到代码提示无法弹出的问题,可以通过检查 Rider 设置(如自动补全选项、Unity 插件安装、索引设置)、Unity 项目设置(如解决方案正确关联、脚本导入设置)以及环境和依赖关系(如 .NET SDK 版本兼容性、Unity 和 Rider 版本兼容性)等方面进行排查和解决。
1602 5
|
11月前
|
存储
探索数据结构:单链表的实践和应用
探索数据结构:单链表的实践和应用
183 0
|
Web App开发 Ubuntu 安全
Linux中的certutil命令:处理证书与证书数据库的实用工具
`certutil`是Linux下的命令行工具,用于处理X.509证书和证书数据库,常与NSS库配合,服务于Firefox等应用。安装`certutil`可通过`apt-get install libnss3-tools`(Debian/Ubuntu)或`yum/dnf install nss-tools`(RHEL/Fedora/CentOS)。基本操作包括:使用`-L`列出证书数据库中的证书,`-A`添加证书,`-D`删除证书,`-x`导出证书。此外,还能用`-M`修改信任设置,`-C`列出证书链,`-V`验证证书链的有效性。了解这些功能有助于高效管理证书。
|
小程序
微信小程序扫描二维码如何跳转小程序内指定页面并传递参数给目标页面?
微信小程序扫描二维码如何跳转小程序内指定页面并传递参数给目标页面?
微信小程序扫描二维码如何跳转小程序内指定页面并传递参数给目标页面?

热门文章

最新文章