函数调用过程中的栈帧结构及其变化

前言:本文旨在从汇编代码的角度出发,分析函数调用过程中栈帧的变化。

栈帧的简单介绍:

        当某个函数运行时,机器需要分配一定的内存去进行函数内的各种操作,这个过程中分配的那部分栈称为栈帧。下图描述了栈帧的通用结构。栈帧是一段有界限的内存区间,由最顶端的两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针(也就是说寄存器%ebp保存了所分配内存的最高地址,寄存器%esp保存了所分配内存的最低地址)。当程序执行时,栈指针(栈顶)可以移动,因此大多数信息的访问都是相对于桢指针的。

                          
函数调用前:

        在函数被调用之前,调用者会为调用函数做准备,具体来说就是传参。准备被调用函数需要的参数。就上图而言,当前帧是为调用函数而开辟的栈帧,而参数1~参数n就是调用者传给被调用的参数,在当前帧中体现为设置参数构造区域。我们可以看到在当前帧上面仅挨着的是一个返回地址,这个返回地址是什么?
        调用者在调用完函数以后,肯定需要从下一条指令接着执行。而这个地址就是下一条指令的地址。调用函数的指令为:

call <函数名>。这里的call就实现了下一地址的储存。具体来说,call指令执行时,先把下一条指令的地址入栈,再跳转到对应函数执行的起始处。
        再举个具体的例子,如下图:


                
        我们看一下主函数调用sum()函数的过程:

                 

        我们看到<+37>这一行再调用sum()函数。我们先看之前的几行:先设置变量值,再设置参数构造区,最后再调用函数。虽然这只是一个简单的例子,但是对于绝大数的调用来说,都符合这个规则,就是在调用函数前的几行,做的事情就是构造函数的参数。设置完成后再执行call。执行call的时候,先把下一条指令的地址入栈,再跳转。
被调用函数运行时:

        这里直接对代码进行说明。我们先查看sum函数的汇编代码:

                

        我们看到函数的前两行:

                                    push    %ebp

                                    mov     %esp,%ebp

        对于绝大部分函数来说,前面的两行都跟这两行一样,我们来具体分析这两句干了什么:

        首先是 push    %ebp。当前%ebp保存的是调用者栈帧的栈底地址,那么push    %ebp就是将调用者栈帧的栈底地址压入栈,即保存旧的%ebp。

        接着是mov    %esp,%ebp。我们刚刚把旧的%ebp的值保存了下来,但是%ebp值并没有发生改变,而我们现在在执行一个新的函数,那么%ebp保存的应该是新的栈帧的栈底。所以才把当前%esp储存的地址赋值给%ebp。(这里说明一下,对于每一次push操作,%esp储存的地址会-1)。那么这样一来的话,相当于调用者栈帧的栈顶现在作为了新的栈帧的栈底(并且该栈底保存的是调用者栈帧的栈底地址,请记住这一点)。而此时新的栈帧的栈底和栈底位于同一个位置。

       而我们在函数里面是要执行各种操作的,所以我们需要给新栈帧分配一定的内存。这也就是后面接着的:sub $0x10,%esp。将%esp低地址移动16个字节。有了这么多的储存空间,才能支持函数里面的各种操作(也就是图中所述)。其实在这之前,还可能有一些push 语句,比如push %ebx之类的。这些push操作的目的其实同push %ebp差不多,都是保存调用者的值,以便在函数运行完以后再恢复数据。

        再接着就是利用储存空间执行具体的操作了。最后说明一点的是,函数的返回值一般储存在%eax寄存器中。这个看上述代码也可以看出来:在执行sum()函数时,最后操作完的结构储存在%eax寄存器中。在执行完sum()函数以后,后面<+45>那一句就是将%eax寄存器的值赋值给主函数中的一个参数。

被调用函数运行结束时:

        这里主要对:leave和ret指令进行分析。

        首先是leave指令:在许多的地方都可以找到它的解释:用leave指令可以使栈做好返回的准备,它等价于下面的代码序列:

                                    movl    %ebp    %esp

                                    popl    %ebp

        然而当时我看了之后还是不是很理解,所以这里再进行进一步解释。

        首先是movl    %ebp    %esp。当前%ebp保存的是什么?没错,当前栈帧的栈底地址,所以这一句话的作用就是把%esp给放回到调用者栈帧的栈底。联系到进入函数时的语句movl    %esp    %ebp,其实这就是个逆过程,旨在恢复原来栈顶的状态。

        然后是popl    %ebp。popl是对栈顶元素进行出栈,而现在的栈顶(也是栈底)储存的是什么呢(上面请大家记住的东西就派上用场了),储存就是调用者栈帧的栈底地址。popl    %ebp就是把这一地址赋值给%ebp(其实这个也可以看作push    %ebp的逆过程),所以这一句话就是恢复调用者栈帧的栈底。这样一来的话调用者栈帧就基本上是恢复到原来的状态了。
        然后呢,显然上面也说了leave只是做好返回的准备。准备什么呢,我们调用完函数以后,调用者还需要接着向下执行指令,那么调用完函数以后就应该跳转到该函数的下一条指令的地址。这么跳转?还记得我们的call指令吗--先将下一条指令的地址入栈,然后跳转。这里ret的作用就是把哪一个地址给弹出栈,并且跳转到地址对应的语句,再接着执行,这样以来一个函数就完整地运行结束了。
        对照最开始地那个结构图来说,这两条语句地作用就是:(leave指令)先将栈指针%esp移动到桢指针%ebp,然后把被保存地%ebp赋值给寄存器%ebp(此时%esp+1,指向返回地址)。(ret指令)然后把返回地址出栈,并跳转到返回地址对应地指令。

总结:

        函数调用过程中的栈帧变化是学习汇编是必须掌握的一个知识点,当时老师将的时候没有听得特别认真。所以线下自己去弄懂还花了点时间的。对于不熟悉的人来说,这个过程可以一遍两遍还是不怎么理解,但是只要多去想想,查查资料,问问同学,还是可以弄懂的。
————————————————
版权声明:本文为CSDN博主「AC-NEWBIE」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xbb224007/article/details/80106961

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值