探秘函数栈帧:从创建到销毁的底层之旅

1.什么是函数栈帧?

函数栈帧,是程序运行时调用函数时,在内存栈(Stack)中为函数分配的一块连续内存区域,用于存储函数的局部变量参数返回地址以及其他函数调用相关的信息,确保函数能够正确执行并返回到调用点继续后续操作。

2.理解函数栈帧的作用有哪些呢?

***局部变量是如何创建的呢?

***为什么局部变量的值是随机的呢?

***函数是怎么传递参数的呢?传参的顺序是怎样的呢?

***形参与实参的关系是怎样的呢?

***函数调用是怎样的呢?

***函数调用后如何返回的呢?

当你读完了这篇文章,会将这些问题一一破解。

3.手撕函数栈帧  

      (1)栈帧相关的寄存器

         那么什么又是寄存器呢?寄存器时CPU内的高速存储单元,用于临时存取数据和指令,直接影响计算机的性能。

        寄存器的种类很多:

         >>通用寄存器(存操作数/中间结果)

         >>指令指针(指向下一条指令地址)

         >>堆栈指针(管理函数调用和局部变量)//显而易见本文会用到与此相关的指针

         >>状态(记录运算标志)

         >>专用(控制寄存器,段寄存器)

      下面将会介绍本文后续讲解中用到的最主要的两个寄存器:

       (1)esp 栈顶指针 :指向栈顶,动态变化。

       (2)ebp 栈底指针:指向当前栈帧的基地址,用于访问参数与局部变量。

          这里的栈顶与栈底可以看下面我的示意图进行理解:

          

在后面的讲解中会用到一些其他的寄存器,例如ebx,edi,esi等,如edi(指向目的字符串的首地址,esi(指向源字符串的首地址),理解这些会使你对C语言底层机制有更深的理解,但理解本文内容主要在于理解esp与ebp。寄存器相关内容与计算机汇编语言编程有着密不可分的关系,所以下面我会说明一下理解本文内容需要的一些汇编语言知识。

(2)汇编语言

汇编语言是一种面向机器的低级程序语言,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。

特点:执行效率高,能直接访问硬件。

缺点:编程难度大,可移植性比较差。

应用领域:主要用与对硬件的底层控制,编写操作系统的内核,及对性能要求比较高的程序模块。

行文至此,我会先介绍两个最常见的汇编指令,并用具体的汇编译语言场景,帮助你理解。并列举出本文会用到的指令,了解其基本作用,在后面的程序语言中进行理解。

>>数据传输指令

1.mov 用于将数据从源操作数移向目的操作数

2.push 把数据压入堆栈

                                                          看到第一个move操作

eax是这里的目的操作数,源操作数是dword ptr[ebp-14h],表示从寄存器ebp减去14h的值,为地址的内存单元中取出四个字节的,32位的数据,传输到寄存器eax里

                                                          看到第二个push操作,

该指令将eax寄存器中存放的数据压入堆栈中。需要注意的是压栈过程中遵循的原则:esp的地址首先会递减,然后数据会被压到esp指向的内存单元

下面我会以图示的方式让你理解内部运行的机制:

                                                           图(1)

>>控制转移指令

1. jmp :无条件跳转指令

2. call :调用子程序指令

3. ret :从子程序返回指令

指令的内容读者如果有兴趣,可以自行研究,这里我们只需要知道每一步的作用即可,切忌钻牛角尖,重在理解思想即可

为了理解这两个过程我将用VS2013观察这些操作,每个编译软可能会有不太一样的执行效果,但道理相同,读者可以尝试使用vs2019。这里建议使用版本较低的编译器,由于其对函数优化程度相对于编译器版本较高的比较差

光说不练假把式,学习编程实践是必不可少的一环。

下面以vs2013里的一个代码为例

                                                           图(2)

代码很简单,实现的是两个数字加和,为了理解函数栈帧的创建与销毁过程,我们进入调试模式,观察调用堆栈,内存变化等,我会结合相关图例帮助你理解。

1.F10进入调试模式,打开窗口里的调用堆栈,不断逐语句运行,可以想到main函数作为一个函数也是被调用的,那么是谁呢?这里调用堆栈窗口会给出答案。

                                                             图(三)

如箭头所指,main函数是被该函数调用,那么你可能会,那么谁又调用了这个函数呢?

没错,看到调用堆栈下(行466),显而易见该函数是被mainCRTStartup()调用

见下图

                                                                图(4)

这里大家弄明白了函数调用的关系了吧,那么接着跟着我一起探索汇编语言的魅力吧!!!

这里的学习更多的体会思想,不要过多纠结于一个小点,避免陷入死胡同出不来!!!

下面看到这段代码的汇编语言:

前面已经大概讲解了函数创建栈帧及栈顶指针与栈底底指针的内容,下面我会以容易理解方式带你领会思想,感受过程。

回到图一显示了main函数栈帧的创建,可以想到同样方式__tmainstartup与mainstartup函数栈帧的创建,这里无需考虑过多细节,思想,思想,理解思想远比钻牛角尖有意义!

 当你理解了这幅图你就理解了函数栈帧的创建销毁的精髓了

上图可以解释我们开始提到的所有问题,希望读者可以加深对这个图片的解读,理解思想很重要,再次重申,不要拘泥于细节!!因为不同编译器会有不同的操作!!!!

上图形象展示了函数调用的全过程,由于汇编代码代码量原因,这里就不进行展示,希望读者可以自行实现,配合示意图理解。

下面我会补充函数返回的一些过程关键点,见下图

pop 退栈操作,push的反动作,读者结合我的示意图可以理解。调试进入 ret

函数进行返回

函数回到上图中提到的 call指令的下一个地址,以确保函数返回时找到最开始出发的地方,这就是魅力所在之处,有来也有去。

add sub的反义操作,聪明的读者如果能读到这里,想必已经理解了如何思考函数栈帧创建与销毁了,紧接着,函数用到的变量返回给系统,数据被存放到eax寄存器中,进行返回。

​​行文至此,函数栈帧的创建与销毁的探索之旅到此结束,理解起来会有一定的难度,但只要大家想弄明白一定是可以的!!!

诸君,共勉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值