当前位置: 首页 > Linux

C函数调用过程原理及函数栈帧分析

时间:2023-04-07 01:13:57 Linux

在x86计算机系统中,内存空间中的栈主要用来保存函数参数、返回值、返回地址、局部变量等,所有的函数调用都必须push或pop不同的数据和地址进出栈。因此,为了更好地理解函数调用,我们需要先了解一下栈是如何工作的。什么是堆栈?简单的说,栈就是一种后进先出形式的数据结构,所有的数据都是后进先出的。这种数据结构形式正好满足我们调用函数的方式:父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将数据从栈中弹出并存入指定的寄存器或内存中。这是推送操作的示例。假设我们有一个栈,其中黄色部分是已经写入数据的区域,绿色部分是还没有写入数据的区域。现在我们将0x50压入栈中://Push0x50ontothestackpush$0x50我们来看出栈操作的例子://Pop0x50offthestackpop这里有两点需要注意,首先,在上面的例子中栈的增长方向是从高地址到低地址。这是因为在下面说的栈帧中,栈是向下增长的,所以这里也使用了这种栈形式;第二,pop操作之后,stack中的数据并没有被清除,只是我们不能直接访问数据。有了这些栈的基础知识,我们现在就可以看看x86-32bit系统下C语言函数是如何调用的。什么是栈帧?栈帧,即栈帧,本质上是一种栈,只不过这种栈是专门用来保存函数调用过程中的各种信息(参数、返回地址、局部变量等)的。栈帧分为栈顶和栈底。栈顶地址最低,栈底地址最高。SP(栈指针)总是指向栈顶。在x86-32bit中,我们使用%ebp指向栈底,也就是基地址指针;使用%esp指向栈顶,也就是栈指针。下面是一个栈帧的示意图:一般来说,我们把%ebp和%esp之间的区域看成一个栈帧(有人认为应该以函数参数开头,但这不影响分析)。整个栈空间不只有一个栈帧。每次调用一个函数,都会产生一个新的栈帧。在函数调用的过程中,我们把调用函数的函数称为“调用者(caller)”,被调用的函数称为“被调用者(callee)”。在这个过程中,1)“调用者”需要知道从哪里得到“被调用者”返回的值;2)“被调用者”需要知道传入参数在哪里,以及3)返回地址在哪里。同时我们要保证“被调用者”返回后,%ebp、%esp等寄存器的值要和调用前保持一致。因此,我们需要使用栈来保存这些数据。函数调用实例函数调用下面通过例子看看函数是如何直接调用的。这是一个简单的函数,它接受参数但不调用任何函数,我们假设它被其他函数调用。intMyFunction(intx,inty,intz){inta,b,c;一=10;b=5;c=2;...}intTestFunction(){intx=1,y=2,z=3;我的函数1(1,2,3);...}对于这个函数,调用时,MyFunction()的汇编代码大致如下:_MyFunction:push%ebp;//保存%ebp的值movl%esp,$ebp;//将%esp的值赋给%ebp,使新的%ebp指向栈顶movl-12(%esp),%esp;//给局部变量分配额外的空间movl$10,-4(%ebp);movl$5,-8(%ebp);movl$2,-12(%ebp);光看代码可能不太清楚,我们先看看此时的栈长什么样子:此时调用者做了两件事:首先将被调用函数的参数按顺序压入栈中从右到左。其次,返回地址被压入堆栈。这两件事都是调用者的责任,所以压入的栈应该属于调用者的栈帧。我们再看看被调用者,它也做了两件事:首先,将旧的(调用者的)%ebp压入栈中,此时%esp指向它。其次,将%esp的值赋给%ebp,%ebp有了新的值,同时也指向存放旧%ebp的栈空间。此时,它成为函数MyFunction()的堆栈帧的底部。这样,我们就保存了“caller”函数的%ebp,并创建了一个新的栈帧。只要清楚这一步,后面的操作就很容易理解了。%ebp更新后,我们先分配一块0x12字节的空间来存放局部变量。这一步通常用sub或mov指令来实现。这里使用了movl。通过使用带有-4(%ebp)、-8(%ebp)和-12(%ebp)的mov我们可以给a、b和c赋值。函数的返回以上就是函数的调用过程。让我们看看函数是如何返回的。从下面的例子中,我们可以看出调用函数时正好相反。当函数完成它的工作时,它将%esp移动到%ebp,然后将旧的%ebp值弹出到%ebp。这样%ebp就恢复到函数调用前的状态。intMyFunction(intx,inty,intz){inta,intb,intc;...return;}程序集大致如下:_MyFunction:push%ebpmovl%esp,%ebpmovl-12(%esp),%esp...mov%ebp,%esppop%ebpret我们注意到最后有个ret指令,相当于pop+jum。它首先从栈中弹出数据(返回地址)并保存在%eip中,然后处理器根据这个地址无条件跳转到对应位置获取新的指令。到这里总结一下,C函数的调用过程就基本结束了。函数调用其实并不难。只要了解%ebp和%esp的保存和恢复,就能理解函数是如何通过栈帧调用和返回的。希望本文对您有所帮助!参考资料在学习栈帧和写这篇文章的过程中,参考了以下几篇文章,在此感谢他们的大力帮助。如果您对这些文章感兴趣,请访问以下链接:1.x86指令集参考2.x86反汇编/函数和堆栈框架3.x86汇编指南