栈帧的简单介绍:
当某个函数运行时,机器需要分配一定的内存去进行函数内的各种操作,这个过程中分配的那部分栈称为栈帧。下图描述了栈帧的通用结构。栈帧是一段有界限的内存区间,由最顶端的两个指针界定,寄存器%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