C/函数栈帧

简介: C/函数栈帧

目录

前言

寄存器

1. 寄存器的种类与功能

C语言汇编指令介绍

函数栈帧的创建与销毁过程

1.函数栈帧的概念

2.函数栈帧的创建与销毁

main函数的创建

Add函数函数栈帧的创建

Add函数栈帧的销毁

后记

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

2.为什么局部变量不初始化的值是随机值?

3.函数是怎么传参的? 传参的顺序是怎么样的?

4.形参和实参是什么关系?

5.函数调用是怎么做的?

6.函数调用结束后怎么返回的?



前言

 我们学习语法学习编程逻辑都是基于封装好的知识上来进行学习,知其然而不知其所以然,如果想要更好的掌握理解所学知识,我们对知识应该有一个更深层次理解,函数怎样传参,调用后怎么返回? 局部变量怎样创建 ?这里就需要我们去了解函数栈帧的创建与销毁;

在此之前我们先了解一下寄存器和部分汇编指令的概念

寄存器

   寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。

1. 寄存器的种类与功能

种类 功能
EAX 累加器(加法乘法指令)
EBX 基址寄存器(在内存寻址时存放基地址)
ECX 计数器(在循环和串操作中充当计数器)
EDX 数据寄存器(在I/O指令中可用作端口地址寄存器,乘除指令中用作辅助累加器)
EBP 寄存器存放当前线程的栈底指针
ESP 寄存器存放当前线程的栈顶指针
ESI 源变址寄存器
EDI 目的变址寄存器

C语言汇编指令介绍

汇编指令 功能
push 压栈操作
mov 指令将源操作数复制到目的操作数
sub 两个操作数的相减,即从A中减去B,其结果放在A中
lea 将存储器操作数mem的4位16进制偏移地址送到指定的寄存器
pop 将操作对象弹出栈帧,出栈
call 调用函数,调用前会将call下一条语句的地址压栈在栈顶
ret 将栈顶的地址弹出并返回到该地址的地方

函数栈帧的创建与销毁过程

ps:每个编译器上的展示可能略有差异,但是大体逻辑是一样的,版本越低的编译器越好观察,编译器越高级越不容易观看整个创建与销毁的过程,因为它的封装过程会复杂一些;这里我用vs2013为大家演示一下。

为了演示函数栈帧的创建与销毁,我们用下面这段简单的代码来观察:

#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", c);
  return 0;
}

1.函数栈帧的概念

函数栈帧:每一个函数调用的时候都需要在栈区上开辟一块内存空间.栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的 一种数据结构。

函数的压栈出栈

push压栈: 每当有一个新的函数被调用的时候,在内存中就会在上一个函数的顶上创建新的函数栈帧,这个就叫压栈,(给栈顶放一个元素)

pop出栈:当函数内代码执行结束时,函数栈帧就会重新归还内存,这叫出栈。(从栈顶删除一个元素)

对于栈,我们都知道栈是由高地址向低地址延伸的,每个函数的每次调用,都有它自己独立的一个栈帧,一个函数栈帧的创建就需要esp和ebp这两个寄存器的维护,ebp存放它的高地址,esp存放它的低地址,这两个地址之间的内存就形成了当前函数的函数栈帧,每当需要调用一个新的函数esp和ebp就会去维护那个新的函数栈帧,两个寄存器里面就会存放两个新的地址.

这样我们就了解了寄存器ebp和寄存器esp中存放的是地址,这两个地址是用来维护函数栈帧的。比如:我们常说调用main函数, 那么我们为main函数分配栈帧空间, 那么栈帧维护如下:

image.gif

2.函数栈帧的创建与销毁

main函数的创建

我们打开调试,选中调用堆栈窗口可以看到,main函数是在__tmainCRTStartup函数内部被调用的,而__tmainCRTStartup函数又是在mainCRTStartup函数内部调用的

image.gif

为了能更加直观看到栈帧创建和销毁的过程,我们转到上面代码对应的反汇编代码:

image.gif

这里就能看出main函数函数栈帧的创建过程

第一步是进行push命令,将ebp压入栈顶 将ebp压入栈顶后,esp的地址也会随之改变。

第二步进行mov指令,将esp的值给ebp

第三步进行sub指令,将esp指向的地址减去0E4h(十进制228)

随后分别把ebx,esi,edi压入栈顶

随后四句指令main函数栈帧初始化 ,把从edi(ebp-0E4h)开始的ecx(39h)个 空间初始化为eax(0CCCCCCCCh)。

image.gif

执行main函数内部代码

image.gif

image.gif

Add函数函数栈帧的创建

当变量abc创建好了之后,就要开始调用add函数。

随后分别将ebx(20)和ecx(10)压入栈顶。

这两个指令实际上是在为Add函数传参

image.gif

image.gif

进入Add函数后,前面的几条指令跟进入main之前的push mov sub 等指令一样,是为了给函数准备栈帧和对其进行初始化

image.gif

Add函数栈帧的销毁

image.gif

第一步edi出栈

第二步esi出栈

第三步ebx出栈

随后将ebp赋给esp

再将以前存着main函数ebp地址的内存弹出到现在的ebp内,使ebp回到main的顶部,系统中也根据刚刚在内存中的存储,使esp再向上走,找到main的底部,此时再通过ret使刚刚调用分支函数和参数拷贝的内存释放,而编译器也会通过call指令的下一条指令的地址回到main函数的原来位置。此时esp向高地址移动8字节,esp,ebp重新维护main函数,eax中存放的返回值将被传递给地址(ebp - 20h)即ret的地址。至此,Add函数返回完毕。main函数栈帧销毁过程与前述过程类似

后记

问题与收获

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

函数栈顿创建后编译器分配由高到低地址创建变量

2.为什么局部变量不初始化的值是随机值?

 函数栈顿创建后会默认将所有内容初始化为0cccccccch:

3.函数是怎么传参的? 传参的顺序是怎么样的?

传参参是将实参值拷贝后进行压栈在栈顶,顺序是由右到左

4.形参和实参是什么关系?

形参是实参的临时拷贝,只是值相同却是不同的地址:

5.函数调用是怎么做的?

call;

6.函数调用结束后怎么返回的?

ret;


好了,本篇学习就到此为止了,如有问题欢迎指正,感谢各位佬的支持!

相关文章
|
5月前
|
编译器
函数栈帧的创建和销毁
函数栈帧的创建和销毁
28 0
|
6月前
|
存储 程序员 编译器
深入理解函数调用--函数栈帧
深入理解函数调用--函数栈帧
|
6月前
|
存储 编译器 容器
函数栈帧的创建和销毁讲解
函数栈帧的创建和销毁讲解
39 0
|
6月前
|
编译器 容器
关于函数栈帧的创建和销毁
关于函数栈帧的创建和销毁
|
存储
函数栈帧的创建和销毁(下)
函数栈帧的创建和销毁(下)
52 0
|
11月前
|
编译器 程序员 C语言
函数栈帧的创建与销毁(超详解)
函数栈帧的创建与销毁(超详解)
101 0
|
12月前
|
存储 缓存 编译器
函数栈帧的创建与销毁
函数栈帧的创建与销毁
37 0
|
存储 C语言 C++
你知道函数栈帧的创建和销毁吗?
你知道函数栈帧的创建和销毁吗?
73 0
|
编译器 C语言 容器
函数栈帧的创建和销毁(一)
函数栈帧的创建和销毁
115 1
|
存储 机器学习/深度学习 编译器
函数栈帧深度剖析(一篇带你牢牢掌握函数栈帧)(一)
函数栈帧深度剖析(一篇带你牢牢掌握函数栈帧)(一)
623 0