Files
Sec-Interview/Chapter23/23-5.md
T
2025-09-25 04:02:19 +08:00

36 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### 函数调用时的流程,参数如何传入以及寄存器、栈的变化
**函数调用前的准备 (Caller)**
在调用函数前,调用方(caller)会进行以下准备:
1. **参数传递**
- **寄存器优先**:对于前几个参数(通常是前4个,具体数量取决于调用约定),它们会被放入特定的通用寄存器中。在 x64 `fastcall` 约定下,参数会依次放入 **RCX, RDX, R8, R9** 寄存器。这种方式非常快,因为它避免了昂贵的内存操作
- **栈传递**:如果参数数量超过了寄存器的限制,剩下的参数就会被从右到左(或从左到右,取决于具体约定)压入栈中
2. **栈帧对齐**:为了保证性能,特别是对于一些高级指令集(如 SSE、AVX),栈帧需要对齐到特定的字节边界(通常是16字节)。调用方会确保在调用 `call` 指令前,栈指针 **RSP** 是对齐的
3. **调用指令**:最后,调用方会执行 `call` 指令。`call` 指令有两个主要作用:
- 将**下一条指令的地址**(即函数的返回地址)压入栈中
- 跳转到被调用函数(callee)的入口地址
**函数执行过程中的变化 (Callee)**
一旦 `call` 指令将控制权转移给被调用函数(callee),它会做以下几件事:
1. **保存旧栈帧**:函数的第一条指令通常是 `push rbp`。这将调用方函数的基址寄存器 **RBP** 的值压入栈中,保存了调用方的栈帧信息
2. **建立新栈帧**:接下来,函数会执行 `mov rbp, rsp`。这会将栈指针 **RSP** 的当前值复制到 **RBP**,从而建立起当前函数的栈帧。从现在开始,所有局部变量和参数都可以通过 **RBP** 加上或减去一个偏移量来访问
3. **局部变量分配**:如果函数有局部变量,它会通过 `sub rsp, [size]` 指令在栈上分配空间。这个操作会使栈指针 **RSP** 向低地址方向移动,为局部变量腾出空间
4. **保存非易失性寄存器**
- **易失性寄存器(Volatile / Caller-saved**`RAX`, `RCX`, `RDX`, `R8`-`R11`等。这些寄存器被认为是临时的,调用方**不指望**它们在函数返回后保持原值
- **非易失性寄存器(Non-volatile / Callee-saved**`RBX`, `RBP`, `RDI`, `RSI`, `R12`-`R15`等。这些寄存器被认为需要保持其值不变。如果被调用函数需要使用它们,就必须在使用前将它们的值压入栈中,并在返回前恢复
5. **执行函数主体**:现在,函数开始执行其核心逻辑。它可以使用传递进来的参数(通过寄存器或栈),也可以使用自己栈上的局部变量
**函数返回时的清理 (Callee & Caller)**
当函数执行完毕,准备返回时,会进行以下清理工作:
1. **恢复栈指针**:函数会执行 `mov rsp, rbp`。这个指令会将 **RSP** 的值恢复到进入函数时的状态,从而**释放所有局部变量**所占用的栈空间
2. **恢复旧栈帧**:接着,函数会执行 `pop rbp`。这会将调用方保存的 **RBP** 值从栈中弹出并恢复到 **RBP** 寄存器中,从而恢复到调用方的栈帧
3. **返回指令**:最后,函数执行 `ret` 指令。`ret` 指令的作用是:
- 从栈中弹出返回地址
-**RIP**(指令指针寄存器)的值设置为弹出的返回地址,从而将程序的控制权交还给调用方的下一条指令
4. **参数清理**:在某些调用约定(如 `cdecl`)中,调用方需要负责清理栈上用于参数传递的空间。然而在 `fastcall` 等现代约定中,由于参数主要通过寄存器传递,这个步骤变得简化。如果参数是通过栈传递的,`ret` 指令后面通常会带一个立即数,告诉 CPU 在返回前额外弹出多少字节的栈空间