函数栈帧的创建以及销毁

        在我们敲代码的途中,我们会创建并且运用各种函数来实现我们需要的功能,但是,大家有没有深入了解函数究竟是如何被编译器使用的呢?今天我们就来深入了解编译器是如何使用函数的吧!

首先,我们需要了解计算机本身的几个结构。

首先,计算机的存储结构分为三种,分别是硬盘,内存,以及寄存器,而我们的编译器就是利用内存以及寄存器来编译函数的。

而内存里又分成了栈区,堆区以及静态区,一般来说,我们的函数的创建以及销毁,以及变量的创建和销毁都是在栈区里面进行的。

而寄存器和内存不同,寄存器是在cpu里面,也是读取数据最快的部分,不用进行IO操作。

寄存器由以下几种:

eax    累加和结果寄存器
ebx    数据指针寄存器
ecx    循环计数器
edx    i/o指针
esi    源地址寄存器
edi    目的地址寄存器
esp    堆栈指针
ebp    栈指针寄存器

接下来让我们来大概了解一下函数是怎么创建以及销毁吧。

当我们创建了main函数并且编译运行的时候,编译器会在栈区中开辟一个一定大小的区域,然后

用寄存器中的ebp指向main函数的底部,esp指向main函数的顶部。

但是当我们新建立一个断点,然后进行调用堆栈的时候,可以发现,这样一条语句:

extern "C" DWORD mainCRTStartup(LPVOID)
{
    return __scrt_common_main();
}

 这条语句告诉我们,main函数实际上是由这个mainCRTstartup语句调用过来的,而我们main函数的return 0就是返回但该函数里面。

之后我们开始深入了解函数栈帧的具体过程。

之前我们说过,main函数实际上是由mianCRTstartup函数引用进来的,因此编译器在栈区里面会先建立mainCRTstartup函数的栈帧,如下:

在这里我们所用来示例的代码如下:

#include<stdio.h>
int Add(int x,int y)
{
    int z = x+y;
    return z;
}
int main()
{
    int a  = 10,b = 20;
    int c = 0;
    c = Add(a,b);
    printf("%d\n",c);
       return 0;
}

 之前我们说过,main函数实际上是由mainCRTstartup函数引用而来的,所以编译器会在栈区中首先将esp和ebp压入栈,并且用esp减去一个数用来移动对应的地址,然后作为栈顶(低地址),ebp指向栈底(高地址),这样就成功开辟出一个空间,之后当main函数开始运行后,编译器会进行下面的操作:

这里的汇编语句实际上是将ebp压栈,然后将esp的值给ebp,这样就会将ebp移动到esp的位置,然后进行sub操作,esp会减去0E4大小的值,这样就开辟出一个空间,给main函数使用,

然后在压入三个元素入栈,分别是ebx,esi,edi,实际上,每压一个元素入栈区,esp都会指向最新的元素的地址,所以压入三个元素入栈后,实际上esp的地址就会指向edi的位置。

 

 上面这张图是压入三个元素后,几个元素所保存的值以及它们的地址,我们可以看到,edi中存储的值是00c2110e,而右边可以看到,这个值的地址是008fab4,正好是esp所存储的值。(疑惑为何值的从右往左看的可以了解一下数据的存储方式)。

之后再看接下来编译器进行了lea操作,(lea操作实际上是加载有效地址),这个操作将ebp-0E4h的值给了edi,而ebp-0E4h实际上就是指main函数的实际空间大小。

接下来的两个mov分别将39h放到ecx中,0CCCCCCCC放到eax中,最后一句操作是说从edi位置开始,向下的39h大小的空间全部设置为0CCCCCCCC大小的值,这样就将main函数内部的全部设为了0CCCCCCCC。而这就是为什么我们设置局部变量的时候需要初始化的原因了,当我们还未初始化局部变量的时候,我们输出的值是一个随机值,而随机值的大小就是看编译的时候,编译器给了多大的数放在main函数内部。

表示如下:

以上就是编译器为main函数创建空间的步骤,之后就开始执行我们的代码

 

 接下来的编译器会遇到我们所创建的局部变量,分别是a = 10,b = 20,以及c = 0;

编译器会在main函数的空间内部分别找到一个地址来将a的值,b的值以及c的值存储,若是没有初始化,那么局部变量的值就是之前所存储的0CCCCCCCC。

然后我们的编译器就遇到了Add(a,b),开始了函数调用操作。

编译器的操作如下

 首先将ebp-14h的值放在eax中去,而ebp-14h的值实际上就是b的值(看上面的图),然后压栈,在将ebp-8的值给ecx中去,也就是a的值,再压栈。

之后进行call指令,我们可以看到地址出现了这样的变化:

 在eax和ecx的值分别被压入栈之后,我们可以看到在它们的顶上又被压了一个值(00c24105)入栈,而这个值就是call指令的下一条指令的位置,这样就为函数返回提供了一个位置。

进行完call指令后,我们就来到了Add函数内部,在这里,编译器会进行为main函数开辟空间的操作

 与main函数一样,用ebp和esp来维护函数,其空间大小为0CCh,其中放的值是0CCCCCCCCh。

但是有一点要注意的是,这里第一句指令是push,将ebp的地址压入栈,这里的ebp地址实际上是main函数的ebp的地址。(实际上在main函数调用的时候,栈区里面还压了CRTmainstartup的ebp的地址,只是我忘记画了)

之后碰见了我们为Add函数创建的局部变量,z = 0,并且碰到我们所做的操作z = x+y;

遇到我们写的z=x+y后,编译器进行了三个指令mov add,mov,我们可以看到,编译器首先进行mov指令将ebp+8里的值给了eax,然后进行add指令将eax的值和ebp+0Ch中的值相加,最后进行mov指令把eax的值给到ebp-8的地址上去,而z的地址就是ebp-8,这样就将两个数的和传给了z。

而ebp+8和ebp+0Ch的值就是我们在main函数中压入栈的ecx和eax。

这两个ecx和eax实际上是作为我们的形参使用的,与我们之前在main函数中的a,b的值相同但是地址不同,这就是为什么传值操作形参改变不了实参,而传址操作可以。

之后回到正题;

我们进行完相加的操作后,就会执行return z的指令

这里的mov指令将ebp-8(z)的值放到寄存器的eax中去,这样函数退出的时候结果就不会被销毁了。

后面进行了三个pop指令,分别弹出栈顶元素,然后放到edi,esi,ebx中,这时esp就到了之前ebx的位置,然后将esp移动到ebp的位置,就将编译器为Add开辟的空间全部释放了,然后弹出栈顶元素给ebp,而这里的栈顶元素所存储的值是main函数中ebp的地址,这样ebp就指向main函数的栈底,而esp因为栈顶的弹出而+1,重新指向了main函数的栈顶。

然后编译器执行了ret指令, 由于之前在栈里存储了call指令的下一条地址,所以执行了ret后就会弹出栈顶的地址并且直接从call指令的下一条地址继续执行。

然后我们成功返回了main函数后,我们在栈中压入的ecx以及eax(形参)就没有用了,我们需要删除,编译器就执行了下面的语句:

 首先执行add指令,让esp+8,之前我们压入栈的两个形参eax和ecx的地址正好是8,esp+8后,就将这两个变量的空间给释放了。

然后执行下面的mov指令

 这里是将eax的值放到ebp-20h中去,eax是在Add函数里面计算出的结果,而ebp-20h是main函数中的c的地址,这样就成功将函数的返回值给了main函数的局部变量c中去了。

然后编译器碰见了main函数中的return 0,开始执行main函数的栈帧的销毁,与Add函数一样,就不再赘述了。

总结

1.局部变量如何创建?

利用ebp的偏移量在函数对应的栈区空间输入值

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

因为编译器在创建栈帧的时候,不仅会设定空间的大小,也会把空间里的地址上全部赋上随机值,这个值每一次编译时都不一样。

3.函数如何传参?

编译器会利用寄存器,在函数调用之前,从右往左将实参的值赋给寄存器,然后压入栈中,之后在使用形参的时候就会通过指针的偏移量直接使用

4.形参和实参的关系?

形参是实参的临时拷贝。值相同,但是空间独立,除非传址操作,让形参实参建立关系

5.函数调用如何返回?

在函数调用之前,编译器会提前将main函数中所执行的call指令的下一条指令的地址压入栈中,然后将main函数的栈底的地址压入栈中,这样在返回的时候通过pop指令将弹出地址赋给ebp,这样ebp就能直接返回main函数的栈底,并且成功找到函数调用之前mian函数执行的指令的地址(不是在main函数中调用也时这样,都是存的上一个函数的栈底地址和call指令的下一条指令的地址),而返回的值就放在寄存器中,将其赋给main函数中接收返回值的变量。

这就是函数栈帧的全过程。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值