函数调用时的流程,参数如何传入以及寄存器、栈的变化

函数调用前的准备 (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 在返回前额外弹出多少字节的栈空间
Copyright © 版权信息 all right reserved,powered by Gitbook该文件修订时间: 2025-09-25 03:13:06

results matching ""

    No results matching ""