栈帧、RBP 与 RSP 的解释(大白话)
大局观:一个类比
想象你正在解一道很大的数学题,你手边有一叠草稿纸。
- 栈(Stack):就像电脑的内存栈,是一个临时的工作区。
- 一张新纸:当你调用一个函数(例如
main调用hello),你会拿一张新的纸放在最上面。这张新纸就是hello函数的 栈帧(Stack Frame)。它是这个函数调用的私有工作区。 - 后进先出(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
解释:
push rbp:保存旧的锚点(main的 RBP)。mov rbp, rsp:设置新的锚点,把 RBP 定在当前栈顶。现在 RBP 就是hello的栈帧底部。sub rsp, 0x20:为局部变量分配 32 字节空间。
此时 hello 拥有了自己的“纸张”,可以安全使用。
2. 函数尾声(Epilogue):清理并返回
当 hello 执行完毕,需要收拾现场并返回到 main。典型尾声是:
leave
ret
解释:
-
leave等价于:mov rsp, rbp:丢弃整个栈帧,栈指针回到 RBP。pop rbp:恢复旧的 RBP(即main的锚点)。
-
ret:返回到调用点。call hello时,CPU 已经把返回地址压到栈里。ret会把它弹出并跳回去继续执行。
总结:调用栈(Call Stack)
当一个函数调用另一个函数,再调用下一个函数(比如 main -> functionA -> functionB),栈里会依次叠放多个栈帧。这就是 调用栈。
每次函数返回,最上面的栈帧就被移除,直到回到 main。
调试器里的 bt(backtrace)命令就是把这堆栈帧按顺序打印出来。
这样你就能理解:
- RSP 是动态指针,总在变。
- RBP 是稳定锚点,用来定位局部变量和参数。
- 函数序言和尾声 就是建立和拆除栈帧。