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

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

文章目录

前言

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

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 里


相关文章
|
6月前
|
存储 编译器 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
|
6月前
|
存储 安全 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
|
5月前
|
编译器
函数栈帧的创建和销毁
函数栈帧的创建和销毁
28 0
|
6月前
|
存储 编译器 容器
函数栈帧的创建和销毁讲解
函数栈帧的创建和销毁讲解
40 0
|
程序员 编译器 C语言
细谈函数栈帧的创建与销毁
我们在写C语言代码时,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数如何调用?函数的返回值如何返回的?函数参数是如何传递的?这些问题都与函数栈帧有关系。
178 0
|
6月前
|
存储 编译器
初识函数栈帧的创建与销毁(笔记)
初识函数栈帧的创建与销毁(笔记)
|
编译器 C语言 容器
函数栈帧的创建和销毁(一)
函数栈帧的创建和销毁
115 1
|
存储 编译器 C语言
函数栈帧的创建与销毁(上)
函数栈帧的创建与销毁(上)
|
存储 编译器 程序员
C语言代码函数栈帧的创建与销毁(修炼内功)
目录 在前期的学习中我们可能有很多困惑 例如:局部变量是怎么创建的 为什么局部变量的值是随机值 函数是怎么样传参的 传参的顺序是什么 形参和实参的关系是什么 函数调用是怎么做的 函数掉调用结束后怎么返回的 这篇博客我们来修炼自己的内功,掌握好这篇博客的大部分知识就已经很不错了 我们用到VS2013这个编译器,目的是为了看到更详细的函数封装内容 提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,不是完全相同的,具体细节取决于编译器
|
存储 编译器 C语言
函数栈帧的创建与销毁(反汇编万字讲解)
局部变量是怎么创建的? 为什么未初始化的局部变量的值是随机值? 函数是怎样传参的?传参的顺序是怎样的? 形参和实参是什么关系? 函数调用是怎样做的? 函数调用后是怎样返回的? 我们本章就来研讨这个问题,掌握了函数栈帧的创建和销毁更有利于后期的学习这里建议大家要从头往后一个内容一个内容看,因为这里每一个部分关联性很强!