函数调用的详细过程

前言

本文探讨函数调用时寄存器的行为及栈帧的变化,首先说明 x86 架构下通用寄存器的用途,调用者保存、被调用者保存以及栈帧的概念,然后概括发生函数调用时的一般过程。

x86 架构下通用寄存器的用途

请自行区分操作系统位数和cpu架构位数的区别。x64(x86-64),x86是CPU架构。如果你是x64的CPU装了32位系统,那么也不会使用到x64的寄存器(比如r8d),或者不能完整使用x64CPU的寄存器,比如rax。你只能使用该寄存器的一半:eax

x86 架构下通用寄存器的用途

  • %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中。在 imul 指令中,两个64位的乘法最多会产生128位的结果,需要 %rax 与 %rdx 共同存储乘法结果,在div 指令中被除数是128 位的,同样需要%rax 与 %rdx 共同存储被除数。
  • %rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。sp所指向的栈顶是所能分配的栈空间的极限,如果栈空间不够则需要移动sp指针,分配更多的栈空间。
  • %rbp 是栈帧指针,用于标识当前栈帧的起始位置
  • %rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数(如果有6个或6个以上参数的话)。
  • 被标识为 “miscellaneous registers” 的寄存器,属于通用性更为广泛的寄存器,编译器或汇编程序可以根据需要存储任何数据

调用者保存、被调用者保存

  • 当产生函数调用时,子函数内通常也会使用到通用寄存器,那么这些寄存器中之前保存的调用者(父函数)的值就会被覆盖。为了避免数据覆盖而导致从子函数返回时寄存器中的数据不可恢复,CPU 体系结构中就规定了通用寄存器的保存方式。
  • 如果一个寄存器被标识为”Caller Save”, 那么在进行子函数调用前,就需要由调用者提前保存好这些寄存器的值,保存方法通常是把寄存器的值压入堆栈中,调用者保存完成后,在被调用者(子函数)中就可以随意覆盖这些寄存器的值了。
  • 如果一个寄存被标识为“Callee Save”,那么在函数调用时,调用者就不必保存这些寄存器的值而直接进行子函数调用,进入子函数后,子函数在覆盖这些寄存器之前,需要先保存这些寄存器的值,即这些寄存器的值是由被调用者来保存和恢复的。

栈帧

栈帧,stack frame,其本质就是一段内存空间,专门用于保存函数调用过程中的各种信息(本地局部变量,栈帧状态值(前栈帧的底部),返回地址等)。ebp 和 esp 之间的内存空间为当前栈帧,ebp 标识了当前栈帧的底部,esp 标识了当前栈帧的顶部

函数调用的一般过程

  1. 参数入栈: 将参数从右向左依次压入栈中(或者将参数存入寄存器中);

  2. 返回地址入栈: 将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行;

  3. 代码区跳转: 处理器从当前代码区跳转到被调用函数的入口处;(第 2 、3 步由 call 指令完成)

  4. 保存当前栈帧底部: 将当前 ebp (callee save register) 的值入栈(保存调用者的栈帧底部位置,便于函数调用结束后恢复原函数的栈帧底部位置 push ebp

  5. 更新栈帧底部: 将 esp 值装入 ebp mov ebp esp ,即开辟新栈帧

  6. 给新栈帧分配空间: (向低地址)移动 esp

  7. 执行被调用函数的代码然后保存返回值(在寄存器 eax 中)

  8. 恢复上一个栈帧(弹出当前栈帧):

    • 将 esp 赋值为当前 ebp (movq ebp esp),降低栈顶,回收当前栈帧的空间;
    • 将当前栈帧底部保存的前栈帧底部 ebp 值弹入 ebp 寄存器 (pop ebp),恢复出上一个栈帧;(8.1、8.2 由 leave 指令完成)
  9. 返回: 调用 ret 指令,把 esp 上移一个位置(第 2 步保存的返回地址),将函数返回地址弹给 eip 寄存器。当执行完成 ret 后,esp 指向的是父栈帧的结尾处,父栈帧尾部存储的调用参数由编译器自动释放

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
intfunc_B(int arg_B1, int arg_B2)
{
int var_B1, var_B2;
var_B1=arg_B1+arg_B2;
var_B2=arg_B1-arg_B2;
return var_B1*var_B2;
}

intfunc_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
return var_A;
}

int main(int argc, char **argv, char **envp)
{
int var_main;
var_main=func_A(4,3);
return var_main;
}

过程图示

函数调用的实现

参考