C函数调用过程原理及函数栈帧分析
在x86的计算机系统中,内存空间中的栈主要用于保存函数的参数,返回值,返回地址,本地变量等。一切的函数调用都要将不同的数据、地址压入或者弹出栈。因此,为了更好地理解函数的调用,我们需要先来看看栈是怎么工作的。
栈是什么?
简单来说,栈是一种后进先出的数据结构。这种形式的数据结构正好满足我们调用函数的方式:父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。
两个重要的注意点:
- 栈的生长方向是从高地址到低地址的
- pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问
栈帧是什么
栈帧,本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,我们用ebp指向栈底、用 esp指向栈顶。下面是一个栈帧的示意图:
一般来说,我们将ebp到esp之间区域作为栈帧。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中:
1)“调用者”需要知道在哪里获取“被调用者”返回的值;
2)“被调用者”需要知道传入的参数在哪里;
3)返回的地址在哪里。
同时,我们需要保证在“被调用者”返回后,ebp, esp等寄存器的值应该和调用前一致(堆栈平衡)。因此,我们需要使用栈来保存这些数据。
函数的调用
- 函数调用过程
调用者:
1)将被调用函数的参数压入栈中
2)将返回地址压入栈中
被调用者:
1)将调用者的ebp压入栈,此时esp指向它
2)将esp的值赋给ebp,ebp就有了新的值,它也指向存放老ebp的栈空间
这时,ebp就成了调用者函数栈帧的栈底。如此,我们便将“调用者”函数的ebp保存了下来,同时建立了一个新的栈帧
函数的返回
函数的返回和函数的调用正好相反。当函数完成自己的任务后,它会将esp移到ebp处,然后再弹出旧的ebp的值到ebp寄存器。这样ebp就恢复到了函数调用前的状态了。函数返回时最后的ret指令就相当于pop + jmp。它先将数据(返回地址)弹出栈并保存到eip中,然后处理器根据这个地址无条件地跳到相应位置获取新的指令。


