栈帧、RBP 与 RSP 的解释(大白话)

大局观:一个类比

想象你正在解一道很大的数学题,你手边有一叠草稿纸。

  1. 栈(Stack):就像电脑的内存栈,是一个临时的工作区。
  2. 一张新纸:当你调用一个函数(例如 main 调用 hello),你会拿一张新的纸放在最上面。这张新纸就是 hello 函数的 栈帧(Stack Frame)。它是这个函数调用的私有工作区。
  3. 后进先出(LIFO):当 hello 用完了,你会把这张纸丢掉,露出下面那张(属于 main 的栈帧)。你永远只在最上面那张纸上工作。

[!Caution] 在 x86-64 的内存地址空间 里,栈是向低地址方向增长的。从上往下是高到低.

一个栈帧保存了一个函数运行所需的一切:

  • 该函数的局部变量(例如 hello 里的 int age = 42;)。
  • 传进来的参数(例如 name)。
  • 返回地址(当函数完成后,知道要回到 main 的哪一行继续执行)。

关键角色:RBP 和 RSP

要管理这堆纸(栈),你有两根“手指”:

  • RSP(Stack Pointer,栈指针):就像“写字的手指”。它总是指向栈的最顶端(即当前最低的有效地址,最新分配的空间)。随着你分配或回收数据,RSP 上下移动。在 x86-64 中,栈是向下增长的,所以分配 32 字节时会 sub rsp, 0x20(往低地址移动)。
  • RBP(Base Pointer,基址指针):就像“锚定的手指”。它指向当前函数栈帧的底部。整个函数执行过程中 RBP 保持不动,而 RSP 可能会不断变化。这样一来,局部变量就可以用 相对于 RBP 的固定偏移量 来访问。

例如文档里:

  • [rbp-0x18] 一直指向参数 name
  • [rbp-0x8] 一直指向局部变量 age

执行过程:一次函数调用的步骤

现在我们追踪 main 调用 hello 时栈的变化。

1. 函数序言(Prologue):设置新栈帧

hello 开始时要准备自己的工作区,典型序言代码是:

push   rbp
mov    rbp, rsp
sub    rsp, 0x20

解释:

  1. push rbp:保存旧的锚点(main 的 RBP)。
  2. mov rbp, rsp:设置新的锚点,把 RBP 定在当前栈顶。现在 RBP 就是 hello 的栈帧底部。
  3. sub rsp, 0x20:为局部变量分配 32 字节空间。

此时 hello 拥有了自己的“纸张”,可以安全使用。


2. 函数尾声(Epilogue):清理并返回

hello 执行完毕,需要收拾现场并返回到 main。典型尾声是:

leave
ret

解释:

  1. leave 等价于:

    • mov rsp, rbp:丢弃整个栈帧,栈指针回到 RBP。
    • pop rbp:恢复旧的 RBP(即 main 的锚点)。
  2. ret:返回到调用点。call hello 时,CPU 已经把返回地址压到栈里。ret 会把它弹出并跳回去继续执行。


总结:调用栈(Call Stack)

当一个函数调用另一个函数,再调用下一个函数(比如 main -> functionA -> functionB),栈里会依次叠放多个栈帧。这就是 调用栈

每次函数返回,最上面的栈帧就被移除,直到回到 main

调试器里的 bt(backtrace)命令就是把这堆栈帧按顺序打印出来。


这样你就能理解:

  • RSP 是动态指针,总在变。
  • RBP 是稳定锚点,用来定位局部变量和参数。
  • 函数序言和尾声 就是建立和拆除栈帧。