x64汇编之堆栈工作原理理论篇

📅 2026/8/15 13:55:32
x64汇编之堆栈工作原理理论篇
大家好你们可以叫我凌是个16岁的网络安全学习者。上篇我们讲的内容涉及到了堆栈相信不少人肯定一头雾水。那我们今天就以纯理论的方式讲解下这个东西帮助大家更好地进行理解当然不会涉及到代码因为到后面真正讲到堆栈的时候我们才会引入汇编代码。这篇只是上篇的扫盲及为未来做铺垫。那我们就直接开始吧什么是堆栈堆栈Stack是计算机内存中一块具有特殊访问规则的区域。它的核心规则是后进先出Last In, First OutLIFO。也就是说最后存入堆栈的数据会被最先取出。生活类比想象一叠盘子- 你每次把新盘子放在最上面压栈。- 当需要取走盘子时你总是先取最上面的盘子弹栈。- 你不可能直接从中间抽走一个盘子必须先拿走它上面所有盘子。这就是后进先出。堆栈的行为完全一样。堆栈在计算机中的作用堆栈不是用来长期存储数据的那是硬盘或内存其他区域的任务而是用于临时存放一些关键信息例如- 函数调用结束后应该返回到哪里返回地址。- 函数内部的局部变量这些变量在函数退出后自动失效。- 被保存的寄存器值为了恢复调用者的现场。可以说没有堆栈函数调用就无法嵌套或递归程序将变得非常原始。堆栈的“生长方向”在 x64 架构中堆栈在内存中是向低地址方向生长的。也就是说- 当你往堆栈里放数据压栈栈顶的地址值变小。- 当你从堆栈里取出数据弹栈栈顶的地址值变大。你可以把内存地址想象成一条从上到下的数轴- 高地址在“上边”低地址在“下边”。- 堆栈像钟乳石一样向下生长。这一点与传统思维正好相反通常我们认为“往上增加”但只需要记住压栈 → 地址减小弹栈 → 地址增大。堆栈区的预分配堆栈并非无限大。操作系统在加载程序时会为堆栈预留一定大小的内存区域例如 Linux 默认 8MB。如果程序压栈的数据超过这个预留空间就会发生堆栈溢出Stack Overflow导致程序崩溃。这将在后续章节讨论。小结堆栈是块后进先出的内存区域它向低地址方向生长用于临时存放函数调用相关的数据。为什么需要堆栈你可能会有疑问既然 CPU 有那么多寄存器x64 有 16 个通用寄存器为什么还要专门划出一块内存区域作为堆栈原因有以下几点寄存器的数量仍然不够虽然 16 个通用寄存器比 32 位时代多了不少但它们在函数调用、临时存储、参数传递等场景下依然捉襟见肘。例如- 一个函数可能包含多个局部变量数组、结构体等无法全部存放在寄存器中。- 函数嵌套调用时每个函数都需要保存自己的返回地址和局部变量寄存器会被频繁覆盖。堆栈作为“寄存器扩展”提供了几乎无限的临时存储空间只要不超过操作系统预留的大小。函数调用必须记住“回家的路”当程序调用一个函数时CPU 需要知道函数执行完后应该回到哪里继续执行。这个“返回地址”必须被保存起来并且当多个函数嵌套调用时比如 A 调用 BB 调用 C每个返回地址都要按照“后进先出”的顺序正确返回。堆栈天然的后进先出特性完美匹配这个需求最后一个调用的函数最先返回。假如没有堆栈每次函数调用就只能跳转到固定地址无法实现递归或深层嵌套。局部变量的自动生命周期高级语言如 C、C中函数内部的局部变量在进入函数时分配在函数退出时自动释放。这种“自动”的机制正是通过堆栈实现的- 函数开始时在堆栈上预留一块空间大小等于所有局部变量所需的总和。- 函数退出时这块空间被一次性回收无需手动管理。如果用寄存器或堆来存储局部变量程序员必须手动分配和释放极易出错。函数参数的传递当参数过多时在 x64 调用约定中前 6 个整数或指针参数通过寄存器传递rdi、rsi、rdx、rcx、r8、r9。但如果一个函数需要 7 个或更多参数第 7 个及之后的参数就必须通过堆栈传递。调用者将多余参数压入堆栈被调用函数从堆栈中读取它们。浮点数参数的传递也使用 xmm0–xmm7 超出部分同样用堆栈。保存和恢复寄存器值避免相互覆盖当一个函数准备调用另一个函数时它可能希望保留某些寄存器的值以免被调函数修改。按照调用约定- 一部分寄存器如 rbx、rbp、r12–r15由被调用者保存被调函数如果要用到它们必须先压栈保存返回前再弹栈恢复。- 另一部分寄存器如 rax、rcx、rdx、rsi、rdi、r8–r11由调用者保存调用者如果希望保留这些寄存器的值需要在调用前自己压栈保存调用后再弹栈恢复。这些保存操作同样利用了堆栈。小结堆栈是函数调用机制的核心- 它提供了无限相对寄存器而言的临时存储。- 它自动管理局部变量的生命周期。- 它保证嵌套函数能正确返回。- 它作为补充通道传递过多参数。- 它允许寄存器值被安全地保存和恢复。没有堆栈现代编程语言中的函数、过程、方法、递归等概念都将无法高效实现。堆栈的两个关键指针RSP 和 RBP在 x64 架构中CPU 专门提供了两个寄存器来管理堆栈RSPStack Pointer栈指针和 RBPBase Pointer基址指针。它们不用于普通算术运算而是专门用于记录堆栈的位置。本章只讲解这两个寄存器的概念和作用不涉及如何用指令修改它们。后续章节会结合具体指令演示它们的实际变化。RSP栈指针Stack PointerRSP 始终指向当前栈顶。所谓栈顶就是堆栈中最后一个被压入的数据所在的位置。- 当你向堆栈中压入数据时RSP 的值会减小因为堆栈向低地址生长。- 当你从堆栈中弹出数据时RSP 的值会增大。你可以把 RSP 想象成一张“即时贴”它时刻贴在最新数据的上面。CPU 通过读取 RSP 就知道现在栈顶在内存的哪个地址。在大多数时候你不需要直接修改 RSP压栈和弹栈指令会自动更新它。RBP基址指针Base PointerRBP 通常用于固定地访问堆栈中的数据特别是函数内部的局部变量和参数。为什么有了 RSP 还需要 RBP因为 RSP 会随着压栈和弹栈频繁变化。如果在函数执行过程中你想访问某个局部变量而 RSP 可能已经移动了多次导致计算偏移量非常麻烦。解决办法是在函数开始处把当前 RSP 的值复制到 RBP 中然后以 RBP 为基准通过 固定偏移量 来访问数据。例如- [RBP - 8] 可能指向第一个局部变量。- [RBP 16] 可能指向某个参数。这样即使 RSP 后来上下移动RBP 始终保持不变访问数据就简单可靠。注意RBP 的使用并非强制。现代编译器有时会省略 RBP直接用 RSP 加偏移。但理解 RBP 的作用对理解传统栈帧至关重要。类比RSP 是流动的工人RBP 是固定的标记桩想象你在一个仓库里搬运货物- RSP 就像一个不停移动的推车永远停在当前正在处理的那堆货物旁边栈顶。- RBP 则是在货架上钉的一个固定标记你从这个标记往左数几步负偏移或往右数几步正偏移就能拿到你要的东西。没有 RBP 时你必须记住推车当前的位置再计算离货物的距离很容易出错。有了 RBP就有了一个稳定的参考点。两个指针在函数调用中的典型配合调用前RSP 指向某个位置RBP 无特殊要求。进入函数先把调用者的 RBP 保存到堆栈压栈然后将当前 RSP 的值赋给 RBP这样新的 RBP 就成了当前函数的栈帧基址。函数执行中通过 [RBP - 偏移] 访问局部变量通过 [RBP 偏移] 访问参数。退出函数先恢复旧的 RSP释放局部变量空间再弹出保存的 RBP 值RBP 就回到了调用者的值。这些具体步骤会在后续的章节中详细演示。小结- RSP指向栈顶随压栈/弹栈自动移动。- RBP可选但常用的栈帧基址提供稳定的偏移参考。- 二者配合使得函数可以方便地管理局部变量和参数同时支持嵌套调用。堆栈的基本操作堆栈只有两种核心操作压栈 和 弹栈。压栈Push压栈 是指将一份数据放到堆栈的顶部。- 操作前栈顶指针RSP指向当前栈顶最后一个数据的位置。- 操作中RSP 的值首先减小因为堆栈向低地址生长新位置成为新的栈顶。将数据复制到 RSP 指向的新地址处。- 操作后RSP 指向新压入的数据该数据成为新的栈顶。原有数据不受影响位于它的下方。生活类比往一叠盘子上再放一个新盘子。你先把最上面的位置空出来实际上不需要“空”直接放上去然后把新盘子放在最上面。放好后新盘子成为新的顶部。弹栈Pop弹栈 是指从堆栈顶部取出一份数据。- 操作前RSP 指向当前栈顶最后压入的数据。- 操作中将 RSP 指向的数据复制到某个目标位置例如寄存器或内存。RSP 的值增大移动到下一个数据的位置即之前栈顶的下方。- 操作后原先的栈顶数据被移出但内存中的原值可能仍然存在只是不再被堆栈管理。RSP 指向新的栈顶。生活类比从一叠盘子上拿走最上面的盘子。你先取出最上面的盘子然后剩下的盘子中原来第二位的变成了新的顶部。压栈和弹栈的关键特性- 后进先出最后压入的数据一定最先被弹出。- 自动管理RSP 的增减完全自动不需要程序员手动计算偏移。- 数据不会自动消失弹栈后原来的内存位置上的数据可能仍然存在但下一个压栈操作会覆盖它。- 可以重复弹栈只要栈中有数据就可以连续弹出。堆栈为空的情况当堆栈为空时RSP 指向堆栈区域的最高地址因为堆栈向低地址生长空栈时栈顶在最上面。此时执行弹栈操作会导致错误堆栈下溢因为没有数据可弹。同样压栈操作总是安全的只要不超过预留的堆栈空间。小结- 压栈RSP 减小数据存放到新栈顶。- 弹栈从栈顶取出数据RSP 增大。- 这两种操作维持了堆栈后进先出的特性。函数调用时堆栈的完整变化过程现在开始描述“调用前”到“返回后”的堆栈状态变化。可以结合上面的“压栈/弹栈”的行为想象每步栈顶指针RSP和基址指针RBP如何移动。调用前的准备由调用者完成在调用一个函数之前调用者可能需要做两件事1. 传递参数按照调用约定前几个参数放入寄存器多余参数压入堆栈。参数压栈的顺序通常是从右向左即最后一个参数最先压入。2. 保存某些寄存器的值如果调用者希望保留某些寄存器如 rax、rcx、rdx 等的值不被被调函数破坏它可以在调用前将这些寄存器压栈保存。完成这些后RSP 可能已经减小如果压入了参数或保存了寄存器。执行 call 指令当 CPU 执行 call 指令时它自动完成两件事1. 压入返回地址将 call 下一条指令的地址压入堆栈。此时 RSP 减小 8 字节64 位地址长度。2. 跳转将程序计数器RIP设置为被调函数的起始地址。此时堆栈顶部是返回地址RSP 指向它。函数序言被调函数的开头被调函数开始执行时通常但不强制会执行“序言”序列包括以下步骤1. 保存旧的 RBP将调用者的 RBP 值压入堆栈。RSP 再减小 8 字节。2. 设置新的 RBP将当前 RSP 的值复制到 RBP 中。从此 RBP 成为当前函数的栈帧基址指向栈中保存旧 RBP 的位置。3. 分配局部变量空间将 RSP 减小 N 字节N 为局部变量所需的总空间。这样在 RBP 和 RSP 之间就留出了一块区域用于存放局部变量。此时堆栈布局从高地址到低地址依次为- 调用者的栈帧高地址- 返回地址- 保存的旧 RBP- 局部变量区域RSP 指向其底部函数体执行期间在函数体执行期间- 通过 [RBP - 偏移] 访问局部变量偏移量为正但 RBP 指向旧 RBP 位置所以局部变量在更低地址。- 通过 [RBP 偏移] 访问参数偏移量为正返回地址和参数在高地址方向。由于 RBP 固定不动即使 RSP 因临时压栈如保存寄存器而变化RBP 依然提供稳定的访问点。函数尾声被调函数返回前在返回之前被调函数通常执行“尾声”序列1. 撤销局部变量空间将 RSP 重新设置为 RBP 的值。这相当于释放了局部变量区域RSP 向上移动。2. 恢复旧的 RBP弹栈将保存的旧 RBP 值恢复回 RBP 寄存器。RSP 增加 8 字节。3. 执行 ret 指令ret 自动弹栈将返回地址弹出并跳转到该地址。RSP 再次增加 8 字节。此时堆栈恢复到了调用前的状态如果调用者没有额外清理参数的话。调用后的清理由调用者完成根据调用约定可能需要调用者清理传入的参数如果参数是通过堆栈传递的。例如在 Microsoft x64 调用约定中调用者负责清理堆栈参数而在 System V AMD64 ABI 中参数主要用寄存器传递但如果有堆栈参数也由调用者清理。清理方式很简单调用者将 RSP 增加相应的字节数使 RSP 回到调用前的位置。嵌套函数调用的堆栈行为当函数 A 调用函数 BB 再调用函数 C 时堆栈会逐层增长- 每次 call 压入返回地址。- 每个函数自己的序言压入旧 RBP 并分配局部变量空间。- 返回时按相反顺序释放。从堆栈上可以清晰看到调用链最顶层的栈帧属于当前正在执行的函数下面依次是它的调用者再下面是其调用者的调用者……这正是调试器中 backtrace 命令所显示的内容。小结函数调用过程中堆栈的变化可概括为- 调用前参数准备部分可能压栈。- call自动压入返回地址。- 序言压旧 RBP、设新 RBP、分配局部变量空间。- 函数体通过 RBP 访问局部变量和参数。- 尾声释放局部变量空间、恢复旧 RBP、ret 弹出返回地址。- 调用后调用者可能清理堆栈参数。这套机制使得函数可以任意嵌套、递归而不会相互干扰。堆栈帧的布局图为了直观理解函数调用过程中堆栈的变化下面用一张示意图展示典型堆栈帧的布局。从上到下高地址 → 低地址依次为1. 调用者的栈帧高地址区域调用者函数在执行过程中使用的局部变量、保存的寄存器等。这部分不属于当前函数的栈帧。2. 参数区域仅当参数超过6个时调用者压入的多余参数。在 System V AMD64 ABI 中前6个参数通过寄存器传递第7个及之后的参数按从右向左的顺序压入堆栈。这些参数位于返回地址的高地址侧。3. 返回地址由 call 指令自动压入指向调用者中 call 下一条指令的地址。固定占用 8 字节。4. 保存的 RBP旧基址指针被调函数序言中压入的调用者的 RBP 值。固定占用 8 字节。5. 局部变量区域被调函数用于存放局部变量的空间。大小由函数中局部变量所需的总字节数决定。RBP 减去偏移量访问此处。6. 可能的寄存器保存区被调函数如果需要使用某些非易失性寄存器如 rbx、r12–r15会在此区域保存它们的值。RSP 指向该区域的底部栈顶。7. 其他临时空间如调用其他函数时的参数传递如果当前函数要调用另一个函数可能在此区域构造参数。这是栈顶可扩展的部分。关键偏移规律- 局部变量[RBP - 偏移]负偏移因为局部变量在 RBP 的低地址侧- 保存的 RBP 自身[RBP]- 返回地址[RBP 8]- 参数如果有堆栈参数[RBP 16]、[RBP 24] 等通过这张图可以清晰地看到 RBP 作为“锚点”的作用。无论 RSP 如何移动只要 RBP 不变局部变量和参数的访问偏移就是固定的。堆栈与函数嵌套/递归堆栈的后进先出特性使其天然支持函数的嵌套调用和递归。函数的嵌套调用假设有三个函数A 调用 BB 调用 C。执行流程1. A 被调用A 的栈帧被创建包含返回地址、保存的 RBP、局部变量等堆栈顶部是 A 的栈帧。2. A 调用 BCPU 将返回地址A 中 call B 的下一条指令压栈然后跳转到 B。3. B 创建自己的栈帧压入 A 的 RBP、分配局部变量等。此时堆栈顶部是 B 的栈帧A 的栈帧在其下方。4. B 调用 C类似地压入返回地址跳转到 C。5. C 创建自己的栈帧成为新的栈顶。返回过程1. C 执行完毕它的尾声释放局部变量空间、恢复 B 的 RBPret 弹出返回地址堆栈回到 B 的栈帧顶部。2. B 继续执行完成后类似地返回到 A。3. A 完成后返回到它的调用者。堆栈状态任何时候当前正在执行的函数对应的栈帧位于栈顶其下方依次是它的调用者、调用者的调用者……这正是 backtrace 显示的内容。关键点- 每个函数都有自己的栈帧相互独立互不干扰。- 堆栈的深度等于函数调用的层数未返回的函数数量。- 返回时严格按照“后调用先返回”的顺序。递归函数递归函数是指一个函数直接或间接地调用自身。例如计算阶乘的递归函数。递归调用时的堆栈行为1. 第一次调用 factorial(5)创建栈帧 F5包含参数 5、返回地址、局部变量等。2. 在 F5 中又调用 factorial(4)创建新栈帧 F4压入 F5 中的返回地址F4 位于栈顶。3. 同理依次创建 F3、F2、F1直到基准条件如 n 1。4. 基准条件触发后最顶层的栈帧 F1 先返回其返回值被上一层 F2 使用。5. F2 返回以此类推直到 F5 返回最终结果。堆栈特点- 每一层递归都会生成一个新的栈帧包含该层独立的参数、局部变量和返回地址。- 堆栈深度等于递归调用的次数。- 如果递归深度过大例如没有正确设置基准条件或输入数字太大堆栈会不断增长最终耗尽预留的堆栈空间导致堆栈溢出。与普通嵌套调用的区别- 普通嵌套调用是不同函数之间的调用每个函数只出现一次。- 递归是同一个函数的多次嵌套每次调用创建的同名但不同的栈帧它们彼此独立。堆栈溢出Stack Overflow定义当堆栈的增长超过了操作系统为其预留的空间时栈顶会进入未映射的内存区域触发页错误程序崩溃通常表现为段错误。常见原因- 无限递归递归函数忘记设置终止条件导致无限调用自身堆栈无限增长。- 过深的递归即使有终止条件但输入值过大导致递归层数过多例如计算 100000 的阶乘。- 在栈上分配过大的局部变量例如定义一个 char buffer[1024*1024]1MB 数组如果栈空间不足会立即溢出。预防措施- 确保递归函数有正确的终止条件。- 对于可能深度很大的递归改用循环迭代实现。- 超大数组或数据结构尽量分配在堆上通过 malloc 等而不是栈上。注意堆栈溢出与堆溢出Heap Overflow不同。堆溢出是指向堆内存写入超出分配大小的数据通常不会立即崩溃但可能破坏相邻堆块。小结- 堆栈通过后进先出机制完美支持函数嵌套和递归。- 每次函数调用都会在堆栈上创建一个新的栈帧。- 递归过深或局部变量过大可能导致堆栈溢出程序崩溃。- 理解堆栈的行为有助于避免此类错误并更好地理解程序的调用链。堆栈对齐堆栈对齐是 x64 调用约定中一个容易被忽略但非常重要的要求。什么是对齐对齐是指数据在内存中的起始地址必须是某个数值通常是 2、4、8、16 等的倍数。例如- 2 字节对齐地址末位为 0二进制。- 4 字节对齐地址末两位为 00。- 8 字节对齐地址末三位为 000。- 16 字节对齐地址末四位为 0000。CPU 访问对齐的数据更快某些指令甚至要求数据必须对齐否则会触发异常例如 SSE 指令要求 16 字节对齐。为什么堆栈需要对齐在 x64 调用约定System V AMD64 ABI中规定 在 call 指令执行之前栈指针 RSP 的值必须是 16 的倍数。原因如下- SSE/AVX 指令要求许多 SIMD 指令如 movaps、addps要求内存地址 16 字节对齐。printf 等 C 库函数内部可能使用这些指令如果栈未对齐会导致程序崩溃。- 性能优化对齐的数据能减少 CPU 内存访问次数提高执行效率。- ABI 强制要求调用约定明确规定了这一规则违反即视为非法可能导致难以调试的错误。对齐的时机对齐规则针对的是 call 指令之前的 RSP 值。也就是说当你准备调用一个函数时RSP 必须是 16 的倍数。然而call 指令本身会将返回地址8 字节压入堆栈这会导致 RSP 减少 8。因此在函数入口处即被调函数的第一条指令执行时RSP 的值原本是 16 的倍数减去 8 后变成了 16×N 8即 8 模 16。所以被调函数内部看到的 RSP 不是 16 对齐的这是正常的。如何保证对齐通常在程序的启动点如 _start 或 main 被系统调用时RSP 已经满足 16 字节对齐由操作系统或 C 运行时负责。但是在函数内部再调用其他函数时需要小心维护对齐。常见的对齐方法1. 利用 push 指令push 会减少 RSP 8 字节可能破坏对齐。如果本来 RSP 是 16 的倍数执行一个 push 后 RSP 变为 16×N 8再执行另一个 push 又回到 16 的倍数。因此可以通过控制 push 的次数来维持对齐。2. 显式调整 RSP在调用其他函数之前可以执行 sub rsp, 8 或 add rsp, 8 来强制使 RSP 变成 16 的倍数。例如如果当前 RSP 是 16×N 8则 sub rsp, 8 即可。3. 编译器自动处理如果你使用 C 语言编写代码编译器会自动生成对齐指令。汇编程序员则需要手动保证。典型模式以 main 调用 printf 为例main: push rbp mov rbp, rsp sub rsp, 32 ; 分配局部变量空间 可能的对齐填充 ; ... 调用 printf 等 leave ret注意sub rsp, 32 中 32 是 16 的倍数确保在 call printf 之前 RSP 是 16 对齐的。未对齐的后果如果调用函数时 RSP 不是 16 对齐的可能会发生- 程序崩溃段错误尤其是调用了使用 SSE 指令的库函数如 printf。- 数据异常某些指令会静默忽略对齐问题但效率降低或产生错误结果。- 难以调试崩溃可能发生在被调函数内部与调用点看起来无关排查困难。小结- x64 调用约定要求 call 前 RSP 必须是 16 的倍数。- 原因在于 SIMD 指令的对齐要求和性能优化。- 可以通过调整压栈次数或显式加减 RSP 来保证对齐。- 未对齐会导致程序崩溃或难以预料的行为。堆栈溢出Stack Overflow堆栈溢出是指程序在堆栈上使用的内存超过了操作系统为其预留的空间导致栈顶进入未映射或受保护的内存区域从而触发异常通常是段错误。堆栈溢出的常见原因无限递归递归函数没有正确的终止条件导致函数不断调用自身每层调用都创建新的栈帧堆栈持续增长直到耗尽。递归深度过大即使递归有终止条件如果输入数值极大如计算 1000000 的阶乘递归层数仍然可能超过堆栈容量。局部变量过大在函数内部定义大数组例如 char buffer[1024*1024] 即 1MB如果多个这样的函数嵌套调用或者单个数组就接近堆栈上限极易触发溢出。无限循环的函数调用函数 A 调用 BB 又调用 A间接递归没有终止条件。堆栈溢出的后果- 立即崩溃大多数情况下程序会收到 SIGSEGV 信号显示“段错误”Segmentation Fault。- 数据损坏如果栈溢出恰巧未立即触发页错误可能会覆盖相邻内存如其他变量的值、甚至代码段导致不可预测的行为。- 安全漏洞经典的“栈缓冲区溢出”攻击可利用此漏洞覆盖返回地址执行恶意代码这是另一门安全课题此处仅提及。如何避免堆栈溢出- 使用迭代代替深度递归能用循环解决的问题尽量不用递归。- 增加堆栈大小某些编译器或操作系统允许设置更大的堆栈例如 Linux ulimit -s但这只是延后问题不治本。- 将大对象分配到堆上使用 malloc或类似机制在堆上分配大数组或结构体而不是栈上的局部变量。- 检查递归深度在调试版本中加入深度计数超过阈值时终止。- 静态分析工具利用工具检测可能的无限递归或超大栈帧。堆栈溢出 vs 堆溢出特性堆栈溢出堆溢出发生区域堆栈函数调用、局部变量堆动态分配的内存触发原因递归过深、局部变量过大写入数据超过已分配内存块的大小崩溃时机通常立即栈顶越界可能延迟破坏相邻堆块安全性历史上被利用于代码注入也是常见漏洞来源全文总结- 堆栈的本质后进先出的内存区域向低地址生长。- 为什么需要堆栈寄存器不足、存储返回地址、管理局部变量、传递多余参数、保存寄存器值。- 关键指针 RSP 和 RBPRSP 指向栈顶RBP 用作固定的栈帧基址。- 压栈与弹栈压栈使 RSP 减小数据存入弹栈取出数据RSP 增大。- 函数调用时的完整过程参数准备 → call 压入返回地址 → 序言保存 RBP、分配局部空间 → 函数体通过 RBP 访问变量 → 尾声恢复 RBP、ret → 调用者清理。- 堆栈帧布局高地址依次为调用者栈帧、参数如有、返回地址、保存的 RBP、局部变量、低地址临时空间。- 嵌套与递归堆栈天然支持多层调用递归深度过大会导致溢出。- 堆栈对齐call 前 RSP 必须是 16 的倍数否则可能崩溃。- 堆栈溢出由无限递归或过大局部变量引起后果严重应尽量避免。