函数调用的详细过程
前言
本文探讨函数调用时寄存器的行为及栈帧的变化,首先说明 x86 架构下通用寄存器的用途,调用者保存、被调用者保存以及栈帧的概念,然后概括发生函数调用时的一般过程。
x86 架构下通用寄存器的用途
请自行区分操作系统位数和cpu架构位数的区别。x64(x86-64),x86是CPU架构。如果你是x64的CPU装了32位系统,那么也不会使用到x64的寄存器(比如r8d),或者不能完整使用x64CPU的寄存器,比如rax。你只能使用该寄存器的一半:eax
- %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 标识了当前栈帧的顶部
函数调用的一般过程
-
参数入栈: 将参数从右向左依次压入栈中(或者将参数存入寄存器中);
-
返回地址入栈: 将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行;
-
代码区跳转: 处理器从当前代码区跳转到被调用函数的入口处;(第 2 、3 步由
call
指令完成) -
保存当前栈帧底部: 将当前 ebp (callee save register) 的值入栈(保存调用者的栈帧底部位置,便于函数调用结束后恢复原函数的栈帧底部位置
push ebp
) -
更新栈帧底部: 将 esp 值装入 ebp
mov ebp esp
,即开辟新栈帧 -
给新栈帧分配空间: (向低地址)移动 esp
-
执行被调用函数的代码然后保存返回值(在寄存器 eax 中)
-
恢复上一个栈帧(弹出当前栈帧):
- 将 esp 赋值为当前 ebp (
movq ebp esp
),降低栈顶,回收当前栈帧的空间; - 将当前栈帧底部保存的前栈帧底部 ebp 值弹入 ebp 寄存器 (
pop ebp
),恢复出上一个栈帧;(8.1、8.2 由leave
指令完成)
- 将 esp 赋值为当前 ebp (
-
返回: 调用
ret
指令,把 esp 上移一个位置(第 2 步保存的返回地址),将函数返回地址弹给 eip 寄存器。当执行完成ret
后,esp 指向的是父栈帧的结尾处,父栈帧尾部存储的调用参数由编译器自动释放
示例代码
1 |
|
过程图示
参考
- C/C++:堆栈面面观
- x86-64 下函数调用及栈帧原理
- C++函数调用栈过程
- 0day 安全软件漏洞分析技术(第二版) 第二章 (p40)
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!