【C语言】9.C语言函数栈帧的创建和销毁

C语言函数栈帧的创建和销毁


看完本文你能了解什么?

  1. 局部变量是怎么创建的?
  2. 为什么局部变量的值是随机值?
  3. 函数是怎么传参的?传参的顺序是怎么样的?
  4. 实参和形参是什么关系?
  5. 函数调用是怎么做的?
  6. 函数调用结束后怎么返回的?

寄存器

我们常用的寄存器有eax,ebx,ecx,edx。不过我们这节主要用到的是ebpesp这两个寄存器。

函数栈帧

ebpesp这两个寄存器中存放的是地址。这两个地址是用来维护函数栈帧的。

每一个函数调用,都要在栈区创建一个空间。

在这里插入图片描述

假如说要调用main函数就要在栈区创建一个空间

在这里插入图片描述

这个空间是为main函数开辟的,那么我们就称这个空间是为main函数开辟的一块函数栈帧。

这个空间是ebpesp这两个寄存器来维护的。

在这里插入图片描述

ebp作为寄存器存储的是如图所指向的地址。(寄存器是一块区域,是一个存储空间)

esp作为寄存器存储的是如图所指向的地址。

ebpesp中间的这块空间是由这两个寄存器来维护的。

一般我们把ebp叫做栈底指针,把esp叫做栈顶指针。

为啥叫这个呢?

实际上我们可以把这个看成一个栈,从高地址向低地址是不是那个地址在被使用消耗啊,如果想使用地址只能往上使用,就像栈一样,只能往栈顶放数据。


在vs2013中,main函数也是被其他函数调用的。

main函数被__tmainCRTStartup函数调用,而__tmainCRTStartup函数被mainCRTStartup函数调用。

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("%c\n", c);

	return 0;
}

我们调用main函数,会在栈区分配空间。然后往下走,走到c = Add(a, b);又去调用Add函数去了,给Add函数在栈区分配空间。

这个时候ebpesp这两个指针就去维护Add函数去了。

在这里插入图片描述

那么按照道理,__tmainCRTStartupmainCRTStartup函数在被调用的时候也会有一个对应的函数栈帧。

在这里插入图片描述

右击鼠标转到反汇编

在这里插入图片描述

在调用main函数前,会创建一个__tmainCRTStartup函数栈帧

在这里插入图片描述

进入main函数第一步就是push,也就是压栈。

在这里插入图片描述

压栈操作后,就像这样:

在这里插入图片描述

__tmainCRTStartup函数栈帧上面出现了一个ebp元素。同时esp指针也指向ebp元素的上面了。

下一步是mov,把esp的值给ebp

在这里插入图片描述

这个时候espebp指向了同一个地方

在这里插入图片描述

然后下一步是sub,给esp减去0E4h

在这里插入图片描述

因为下面是高地址,上面是低地址,所以减就意味着esp在图上往上走。espebp之间的那个新的空间就是main函数的栈帧空间。

在这里插入图片描述

然后进行3次push,给顶上压上3个元素。

在这里插入图片描述

然后esp也依次指向ebx上面,esi上面,edi上面。停在edi上面。

在这里插入图片描述

然后进入lea,这里的[]里面的一坨东西看着很费劲

在这里插入图片描述

我们把显示符号名勾上就会变成这样

在这里插入图片描述

ebp-0E4h放到edi里面去,后面两个mov也是把39h放到ecx里面,把0CCCCCCCCh放到eax里面去。

接下来一步比较重要

在这里插入图片描述

这一步是把刚刚edi往下的39h这么多个dword(一个word两个字节,dword是double word四个字节)个数据改成0CCCCCCCCh

压栈(push):给栈顶放一个元素。

出栈(pop):从栈顶删除一个元素。

然后进入mov,把0Ah(也就是10)给到ebp-8的位置上。

在这里插入图片描述

我们假设一个紫色框代表4个字节

在这里插入图片描述

在这里插入图片描述

我们把a=10放进了ebp-8里面,原来的CCCCCCCC就被10替换了。

这也可以说明我们没有初始化的时候内存里面放的就是CCCCCCCC ,之前我们有一节里面打印字符数组没有加/0,导致的打印烫烫烫烫烫烫烫烫就是这个原因。

然后进入mov,把14h放到ebp-14h(相当于ebp-20)上。

在这里插入图片描述

在这里插入图片描述

c的创建也类似,然后我们创建好abc之后,我们调用Add函数。

在这里插入图片描述

我们进入mov,把ebp-14heax。这个ebp-14h不就是b吗?也就是相当于把20给到eax里面去了。

然后push,eax压栈。eax里面放的是20,实际上把20给放进去了。

在这里插入图片描述

然后mov,把ebp-8(也就是a的值10)给了ecx

接着push,ecx压栈。ecx里面放10。

在这里插入图片描述

上面两步其实是传参。

接下来call,就是调用,

在这里插入图片描述

执行call指令会把这个00C21450(也就是call指令的下一条指令的地址)给压栈。

在这里插入图片描述

为什么要压栈这个呢?因为我们call指令执行了之后会进入被调用函数的内部,进去了怎么出来呢?这就要靠call指令的下一条指令了。回来的时候就会找到这个地址,然后从这个地址往下执行。

然后就进入Add函数里面了。

在这里插入图片描述

进入push,把ebp压栈。

在这里插入图片描述

然后mov,把esp的值给ebp。

在这里插入图片描述

然后sub,给esp-0CCh

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

然后继续3次push,3次压栈

在这里插入图片描述

在这里插入图片描述

然后lea,把ebp+FFFFFF34h加到edi里面去。

在这里插入图片描述

然后把33h,放到ecx里面,把0CCCCCCCCh放到eax里面去

然后rep,从edi这个位置开始,向下33h的内容初始化成0CCCCCCCCh

然后进入mov,把0给ebp-8。相当于z=0。

在这里插入图片描述

在这里插入图片描述

然后mov,把ebp+8的值放到eax里面。eax=a=10。

接着add,把ebp+0Ch(也就是ebp+12)的值加到eax里面。eax=10+b=10+20=30。

然后mov,把eax的值放到ebp-8里面。也就是说z从0变成了30。

在这里插入图片描述

在这里插入图片描述

从上面我们可以看到,函数调用的时候没有主动创建x,y这两个形参,而是直接把a,b的参数调用过去了。

传参把a,b的值进行了压栈。

在这里插入图片描述

我们函数传参,还没有调用Add函数的时候我们就先把a,b传过去了,压栈了两个参数。

然后进入Add函数内部进行计算的时候,我们就把压栈的两个参数进行运算。把这两个参数加起来放到z里面去。

所以我们常说,形参是实参的一份拷贝。

然后我们继续mov,把ebp-8的值放到eax(eax是个寄存器)里面去。

我们常说这个zreturn z;在函数调用完就销毁了,销毁了怎么传值出来的呢?就是靠的寄存器传递的,放到这样一个全局的寄存器里面就安全了。

在这里插入图片描述

接下来这个3个pop,就相当于出栈3次。

在这里插入图片描述

在这里插入图片描述

然后我们z的值都给寄存器了,那么Add函数的函数栈帧按道理来说也因该被释放掉了。

然后进入mov,把ebp赋值给esp

在这里插入图片描述

在这里插入图片描述

然后pop,把ebp出栈。这个棕色框就出栈了。然后ebp指针指向的值就是下面的ebp区域了。

在这里插入图片描述

ebp出栈了之后,这个ebp指针指向了下面,因为这个出栈的ebp是为main函数创建的。

在这里插入图片描述

然后esp指针顺势指向了00C21450

在这里插入图片描述

也就相当于我们回到了main函数的函数栈帧里面。

然后是ret。ret指令会让子程序返回到调用函数的代码处。但问题是它怎么知道回到哪里去呢?

在这里插入图片描述

我们注意,现在的栈顶是00C21450,也就是上面说的call指令的下一条指令的地址。ret指令返回的时候其实就是在栈顶返回了call指令的下一条指令的地址。这样才能返回之前的地方。ret后00C21450相当于也pop出去了。

在这里插入图片描述

在这里插入图片描述

然后add,esp+8

在这里插入图片描述

在这里插入图片描述

然后mov,把eax的值给ebp-20h

在这里插入图片描述

也就是把之前Add函数存在eax寄存器里面的返回值给c。

在这里插入图片描述

在这里插入图片描述

c的值从0变成了30。


答案:

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

    在函数栈帧里面划定一块区域给局部变量。

  2. 为什么局部变量的值是随机值?

    因为局部变量不初始化的时候,值是我们随机放进去的。就像这里面的CCCCCCCC。

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

    还没调用函数的时候就push,push,把这两个要用的实参压栈压进去。然后调用的时候通过指针偏量来调用这两个压栈压进去的值。

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

    形参是实参的一份拷贝。但是形参不会影响实参。

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

    上面讲过。

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

    我们在调用函数之前就把call指令的下一条指令的地址压栈压进去了。当我们调用完函数后,弹出ebp,和call指令下一条指令的地址后,esp指针往下走的时候就可以跳转到call指令下一条指令的地址。返回值是通过寄存器来传递回来的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值