想象你走进一家剧院,当晚的节目单上排着十几个节目。每上一个节目,舞台工作人员就要快速搭起一套专属布景;节目一结束,布景又被拆掉,给下一个节目腾地方。这套“搭了拆、拆了搭”的流程,正是 ARM 处理器上函数调用机制的真实写照。在 AArch64 架构中,每次函数调用都会在内存的“栈”上搭起一层“布景”,这层布景就是栈帧(Stack Frame);负责标记布景边界的是帧指针 FP;贴在演员身上、写着“演完该回哪”的纸条,则是链接寄存器 LR。 栈是一块“后进先出”的内存区域——最后搭起的布景最先拆掉,恰好对应函数调用中“后调用的先返回”。AAPCS64(ARM 64 位过程调用标准)把这套搭台拆台的规矩写成了规范:栈往哪个方向长、谁负责看管道具、布景图纸怎么画。理解了这套机制,你就能读懂崩溃日志里那串神秘的调用栈,也能明白递归为什么会爆栈,还能在写底层代码时预判性能瓶颈。

岗位关联

对于 AI 从业者,调试训练框架崩溃、优化推理内核性能、分析算子库的栈溢出,都离不开对调用栈和栈帧的准确理解——这是通往系统底层的必修课。

目录
  1. 可视化演示
  2. 栈帧概念:函数演出的一层布景
  3. 栈指针 SP:追光灯的站位
  4. 帧指针 FP:布景的边界柱
  5. 链接寄存器 LR:演员的回程票
  6. 现场保存与恢复:道具的寄存与归还
  7. 栈帧布局:布景图纸的三块区域
  8. 栈展开与回溯:顺着布景追根溯源
  9. 叶子函数:独角戏的轻装上台
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示AArch64栈帧布局结构、函数序言结语STP/LDP指令、FP链式回溯栈展开及ROP攻击与LR签名防护机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

AAPCS64栈帧机制 栈帧布局:FP/LR区+保存寄存器+局部变量+参数区 序言结语:STP pre-index分配+LDP post-index释放 FP链回溯:[FP]保存前一帧FP形成单向链表 ROP防护:PAC签名+BTI分支目标识别 栈向低地址增长SP递减分配栈空间

栈帧概念:函数演出的一层布景

剧院后台有一块巨大的升降台,每当一个节目上台,工作人员就在台上叠一层新布景。AArch64 程序的运行方式与此如出一辙:每次函数调用,处理器都会在内存的“栈”上为这个函数分配一块连续区域,用来放它的局部变量、临时数据和“回程信息”。这块区域就是栈帧(Stack Frame),每一帧对应舞台上的一层布景。

栈也是一块真实的物理内存空间,由操作系统在进程启动时划出。AAPCS64 规范约定:栈从高地址向低地址“向下”生长——新布景总是叠在低地址方向,函数返回时则原路退回,这就是后进先出(LIFO)。“栈顶”永远是最新一层布景所在的位置,而“栈底”是最初那块固定锚点。

一个栈帧该有多大?规范给出了一张天然的“账单”:

三层面积加在一起,就是这台布景的总占地。演出规模越大,布景越复杂,栈帧自然也就越厚。

frame_size=size(locals)+size(saved_regs)+size(outgoing_args)\mathrm{frame\_size} = \mathrm{size}(\mathrm{locals}) + \mathrm{size}(\mathrm{saved\_regs}) + \mathrm{size}(\mathrm{outgoing\_args})
  1. locals 局部变量区函数自己声明的变量占用的总字节数,相当于这台节目自购的小道具。
    size(locals)\mathrm{size}(\mathrm{locals})
  2. saved_regs 保存寄存器区需要压栈保存的寄存器占用的空间,相当于从隔壁剧组租借道具占的面积。
    size(saved_regs)\mathrm{size}(\mathrm{saved\_regs})
  3. outgoing_args 传出参数区调用下层函数时,参数在栈上预留的窗口,相当于向前台递词的小传话口。
    size(outgoing_args)\mathrm{size}(\mathrm{outgoing\_args})

栈指针 SP:追光灯的站位

舞台上有一盏追光灯,永远只照在“当前正在表演”的位置。处理器的栈指针 SP(Stack Pointer)就是这盏追光灯——它始终指向当前栈顶,也就是已分配空间中地址最低的那一个字节。每一次压栈(push),SP 就向低地址方向移动;每一次出栈(pop),SP 就向高地址方向回缩。

AAPCS64 对 SP 的使用提出了两条硬规矩。第一,SP 必须保持16 字节对齐,如同追光灯的支架必须卡在固定滑轨上,以保证内存访问效率;第二,SP 是“专用”寄存器,不能像 x0 那样当普通计算寄存器随手乱用。函数开头如何调整 SP?就是按照调用所需的栈空间,做一次减法:

灯光师永远知道布景堆到哪一层了——SP 就是这份“实时堆高台账”。

SPnew=SPold−round_up(n,16)\mathrm{SP_{new}} = \mathrm{SP_{old}} - \mathrm{round\_up}(n, 16)
  1. SP_old 进入前栈顶函数入口处,栈指针停在调用者留下的位置,布景还没搭。
    SPold\mathrm{SP_{old}}
  2. round_up(n, 16) 对齐取整把本次需要的空间 n 向上取整到 16 的倍数,满足对齐要求。
    round_up(n,16)\mathrm{round\_up}(n, 16)
  3. SP_new 调整后栈顶从旧栈顶向下减去该值,得到本帧的栈顶边界。
    SPold−round_up(n,16)\mathrm{SP_{old}} - \mathrm{round\_up}(n, 16)

帧指针 FP:布景的边界柱

每个节目的布景两侧都会立起两根红色边界柱,提醒演员“这出戏的天地范围到此为止”。帧指针 FP(Frame Pointer,即 x29)就是这根边界柱:它指向当前栈帧的底部,所有局部变量和保存寄存器的访问都通过“FP + 偏移量”来完成,比如 [FP, #-16] 取局部变量、[FP, #8] 取保存的 LR。

FP 更精彩的设计是链式结构:每个函数在入口处都会把“上一帧的 FP 值”压入栈中,因此 FP 指向的位置存着上一层 FP,层层相扣,像一串灯笼。这条链从最深层的函数一路挂到 main:

需要提醒的是,这根柱子可以“偷工减料”——编译器开了 -fomit-frame-pointer 之后,运行时不再维护 FP,回溯改靠 .eh_frame 调试节中的 CFI 信息来完成。柱子虽省,施工图纸还在。

[FP]=FPprev[\mathrm{FP}] = \mathrm{FP_{prev}}
  1. FP 当前帧指针指向本帧底部,是回溯的起点。
    FP\mathrm{FP}
  2. \[FP\] 取内存方括号表示“读取 FP 地址处的值”,该处正是入口时压入的旧 FP。
    [FP][\mathrm{FP}]
  3. FP_prev 上一帧指针读出来的旧值,就是调用者帧的 FP,链由此向前延伸。
    FPprev\mathrm{FP_{prev}}

链接寄存器 LR:演员的回程票

演员上台时,口袋里都揣着一张回程票,写着“演完这场,回后台第几号化妆间”。链接寄存器 LR(Link Register,即 x30)就是这张回程票,里面保存着函数调用发生处的下一条指令地址。调用指令 BL func 执行时,硬件自动完成两个动作:把 PC+4(当前指令的下一条)写进 LR,然后跳转到 func 的入口。

如果函数一层套一层,问题就来了:funcA 调用 funcB 时,LR 会被 BL 指令改写为“funcA 中 BL 的下一条”。若不提前保存,原本身为“main 回程票”的地址就被冲掉了。因此非叶子函数必须在入口处把 LR(连同 FP)一起压栈,标准的开场白是 STP x29, x30, [SP, #-16]!:

票根只有一张,嵌套演出时必须找地方寄存——栈上那 16 字节的 FP+LR 组合,就是最可靠的票务寄存处。

LR=PC+4\mathrm{LR} = \mathrm{PC} + 4
  1. PC 当前程序计数器BL 指令正在执行时,PC 指向该指令自身所在地址。
    PC\mathrm{PC}
  2. 加 4 指向下一条AArch64 指令定长 4 字节,PC+4 就是紧接着 BL 的那条指令地址,即返回点。
    PC+4\mathrm{PC} + 4
  3. 写入 LR硬件自动把该返回地址存入 x30,完成“发回程票”动作。
    LR=PC+4\mathrm{LR} = \mathrm{PC} + 4

现场保存与恢复:道具的寄存与归还

后台有个规矩:从道具房借出的戏服道具,谁借的谁还。AAPCS64 把通用寄存器分成两队来落实这条规矩。被调用者保存寄存器(Callee-saved,x19–x29 以及 v8–v15 的低 64 位):函数若要使用它们,就必须先压栈保存,返回前再恢复原值——如同借来的道具必须原样归还。调用者保存寄存器(Caller-saved,x0–x18 等):属于“消耗品”,被调用者可以随意改写,调用者若在调用后还指望它们,得自己提前保存。

恢复的顺序必须与保存顺序严格相反,如同“后借的先还、先借的后还”。每一个被保存的寄存器,在栈帧中都占据一个固定的“寄存位”,恢复时按偏移写回:

道具全部归位,下一场戏才能开演——这正是 ABI 保证函数间互不干扰的基石。

ri=[FP+offi]r_i = [\mathrm{FP} + \mathrm{off}_i]
  1. r_i 待恢复寄存器i 表示第 i 个来自 callee-saved 集合的寄存器。
    rir_i
  2. FP + off_i 寄存位地址每个寄存位在栈帧内偏移固定,由编译器在编译期算出。
    FP+offi\mathrm{FP} + \mathrm{off}_i
  3. \[……\] 读取并写回从该地址取回原值,寄存器瞬间“回到演出前状态”。
    [FP+offi][\mathrm{FP} + \mathrm{off}_i]

栈帧布局:布景图纸的三块区域

一张标准的布景图纸,总是划分成三个功能块:最下方是“传词窗口”(传出参数区),中间是“道具寄存架”(保存寄存器区),上方是“舞台道具区”(局部变量区)。AAPCS64 的典型栈帧从上到下正是这样组织的:高地址端是局部变量区,往右下方是保存寄存器区,再往下是给下层函数预留的传出参数区。栈帧边界由 FP 和 SP 围成。

函数入口的“开场动作”通常是标准两步:先 STP x29, x30, [SP, #-16]!——SP 下移 16 并把旧 FP 与返回地址 LR 写入新帧底部;再 MOV x29, SP——让 FP 指向新帧底部。从此以后,[FP] 是旧 FP,[FP+8] 是 LR,而 FP 上方便是局部变量的天下:

图纸画得清楚,搭台和拆台都不会乱套。编译器所有的“地址计算”都在照着这张图纸定位。

[FP]=FPprev,[FP+8]=LR[\mathrm{FP}] = \mathrm{FP_{prev}}, \quad [\mathrm{FP}+8] = \mathrm{LR}
  1. 首条 STP 指令动作以 ! 后缀实现先减后存,SP 一次性下移 16 字节(两寄存器位)。
    SP←SP−16\mathrm{SP} \leftarrow \mathrm{SP} - 16
  2. 写入旧 FP[新SP] 处存放调用者的帧指针,成为回溯链的一环。
    [FP]=FPprev[\mathrm{FP}] = \mathrm{FP_{prev}}
  3. 写入 LR 返回地址[新SP+8] 处存放回程票,函数返回前从这里取回。
    [FP+8]=LR[\mathrm{FP}+8] = \mathrm{LR}

栈展开与回溯:顺着布景追根溯源

剧场里发生安全事故,导演如何快速还原“哪个节目出了问题、当时是谁在台上”?答案是顺着布景边界柱的编号逐层排查。调试器打印的调用栈(backtrace)正是如此:从当前 FP 出发,读取 [FP] 取得上一层 FP,再读 [新FP] 取得再上一层……一路追到 main。这条回溯链在异常处理(Stack Unwinding)、perf 采样分析、日志堆栈打印中无处不在:

如果编译器省略了 FP(柱子被拆掉),回溯器也不至于抓瞎:它会让开 .eh_frame 里的 CFI(Call Frame Information)记录,按“图纸”逐步还原每一帧的范围。FP 链是快速通道,CFI 是兜底方案。

FPi+1=[FPi]\mathrm{FP}_{i+1} = [\mathrm{FP}_i]
  1. FP_i 当前层帧指针回溯起点,来自发生崩溃或采样的那一刻。
    FPi\mathrm{FP}_i
  2. 读取保存的旧 FP方括号取内存,得到的是调用者的帧底地址。
    [FPi][\mathrm{FP}_i]
  3. 迭代到下一层把读到的值赋给 i+1,重复过程直到链头(main)。
    FPi+1=[FPi]\mathrm{FP}_{i+1} = [\mathrm{FP}_i]

叶子函数:独角戏的轻装上台

有些节目是单人独角戏,演员上台不搭布景、不带道具,演完鞠躬转身就走。叶子函数(Leaf Function)就是这种“独角戏”:它不再调用任何其他函数。由于不会触发新的 BL,LR 里的回程票不会被冲掉,因此叶子函数不需要保存 LR,也往往不需要建立栈帧。它直接用寄存器完成计算,一条 RET 就谢幕回家。

这在编译器眼里是块宝贝:避免 prologue(开场存栈)和 epilogue(收场恢复)意味着更少的访存指令和更紧凑的代码。常见的 memset 小型副本、数学库里的 sqrt 包装,都会被编译成叶子形式:

轻装上阵,效率最高——写嵌入式代码、调算子内核时,多留意叶子函数,往往能省下不少时钟周期。

frame_size=0  ⟺  leaf function\mathrm{frame\_size} = 0 \iff \mathrm{leaf\ function}
  1. frame_size 等于 0叶子函数的帧大小为零,SP 不动,栈上不留痕。
    frame_size=0\mathrm{frame\_size} = 0
  2. 双箭头等价关系左侧无帧与右侧“叶子”互为充要条件,编译器据此判定优化策略。
      ⟺  \iff
  3. leaf function 叶子条件函数体内不存在任何对其他函数的调用指令。
    leaf function\mathrm{leaf\ function}

AI 应用场景

编译器自动生成栈帧代码

阶段:AI 编译器代码生成(如 TVM、MLIR 的 AArch64 后端)

解决的问题:AI 框架中的算子(如矩阵乘、注意力)会被编译成底层函数。若手写汇编,人肉计算每一帧的 STP 偏移和栈大小极易出错,且难以维护。AI 编译器利用 AAPCS64 规则自动分配栈帧,自动插入 STP x29,x30,[SP,#-16]! 开场,既保证正确性又便于后续优化。

具体案例:用 TVM 将自注意力算子的 IR 编译到 ARM CPU 上,生成的二进制中每个函数都带规范的 FP/LR 头尾;当算子递归展开中间表达时,编译器按叶子函数规则自动省略部分帧的 LR 保存,代码体积下降约 8%。

嵌入式 AI 部署的栈深度分析

阶段:模型部署与运行时调优(TinyML / RTOS)

解决的问题:在 Cortex-A 系列板卡上跑推理模型时,任务栈大小靠拍脑袋配置往往导致栈溢出或内存浪费。通过 FP 链回溯,可以精确测得最深层调用路径的栈消耗,从而为每个推理任务设置安全栈深。

具体案例:FreeRTOS 上的手势识别任务运行时,利用 backtrace 打印 FP 链,发现某卷积算子的嵌套调用路径峰值消耗 2.4 KB,遂将任务栈从 3 KB 调整为 2.8 KB,既防溢出又省内存。

训练框架崩溃定位

阶段:AI 基础设施稳定性排查与性能分析

解决的问题:PyTorch / TensorFlow 训练进程在 C++ 算子边界崩溃时,日志只有一串地址。借助 FP 链展开调用栈,能快速定位是哪一层算子、哪条调度路径触发了段错误。

具体案例:分布式训练中 A100 节点偶发崩溃,perf record 采样后用 perf report 沿 FP 链还原调用栈,发现是 ncclAllReduce 的深层包装函数在 FP 省略模式下依赖 CFI 展开时出错,最终通过更新编译选项解决。

Python 演示

python40 行
# 模拟AArch64栈帧的压栈/出栈与FP回溯链class StackFrame:    def __init__(self, name, saved_lr, saved_fp):        self.name = name        self.saved_lr = saved_lr  # LR寄存器保存的返回地址        self.saved_fp = saved_fp  # 上一帧的FP帧指针        self.locals = {}          # 局部变量区 class FrameStack:    """栈帧模拟器: 帧从main向funcB逐层压栈"""    def __init__(self):        self.frames = []      # 栈内存, 末尾为当前栈顶        self.fp = -1          # 帧指针, 指向当前帧     def push(self, name, saved_lr):        saved_fp = self.fp if self.frames else 0        frame = StackFrame(name, saved_lr, saved_fp)        self.frames.append(frame)        self.fp = len(self.frames) - 1        print("[push] %-6s SP=%d FP=%d LR=0x%x"              % (name, len(self.frames), self.fp, saved_lr))     def pop(self):        frame = self.frames.pop()        self.fp = frame.saved_fp        print("[pop ] %-6s SP=%d FP=%d 返回 %s"              % (frame.name, len(self.frames), self.fp, frame.saved_lr)) def funcB():    fs.push("funcB", 0x4000a0) def funcA():    fs.push("funcA", 0x4000b8)    funcB()    fs.pop() fs = FrameStack()fs.push("main", 0)funcA()fs.pop()
目的:用 Python 对象模拟 AArch64 栈帧管理中“压栈保存 FP/LR + 出栈恢复 FP”的核心流程。
  1. self.frames.append(frame)新帧压入栈顶,对应汇编中的 STP x29, x30, [SP, #-16]!。
  2. saved_fp = self.fp if self.frames else 0记录调用者的帧指针,FP 回溯链从此开始。
  3. self.fp = len(self.frames) - 1帧指针移到新帧,一个帧一“柱”。
  4. frame = StackFrame(name, saved_lr, saved_fp)把回程票(LR)和老祖宗(FP)写进帧里。
  5. self.fp = frame.saved_fp出栈时恢复上一帧 FP,实现链式回退。
  6. fs.pop()funcA 结束后弹帧,SP/FP 还原到 main,演出落幕。

常见误区

误区“程序里的栈,不是和堆一样往高地址长吗?我看内存地址越用越大。”把栈的增长方向记反,读汇编时会对着 SP 的减法一头雾水。

正解AArch64 的栈明确向下增长:调用函数时 SP 减小,向低地址方向移动。可以这样记:你往舞台布景架上一层一层叠道具,每一层新布景都摆在“更低”的架位上,拆的时候后放的先拆。栈顶是低地址端,栈底是高地址端,SP 永远指向最低的有效地址。

为何常见:日常编程中我们常说“栈空间不够”,却很少关注它在内存里的朝向;加上部分教程画图方向不统一,初学者想当然认为地址越用越大更自然。

误区“规范里写了帧指针,那每个函数都一定有 FP,回溯不就是沿着 x29 一路走吗?”结果在优化代码里找不到 FP,以为程序坏了。

正解AAPCS64 并不强制所有函数都维护 FP。编译器开启优化(如 -O2 配合 -fomit-frame-pointer)时,会省去 FP 的保存与赋值,转而用 SP 相对寻址;此时调试器依靠 .eh_frame 中的 CFI 信息完成回溯。FP 是“可选的便利设施”,CFI 是“无 FP 时的图纸”。

为何常见:教材和入门教程通常展示未优化的调用栈,FP 链给人心智模型太强,让人误以为这是运行时永远存在的硬件结构。

误区“调用函数前必须先把 LR 存到栈里,不然回去的路就丢了。”于是给所有函数都写上 STP,哪怕它根本不再调用谁。

正解只有非叶子函数才需要保存 LR。叶子函数(不再调用其他函数)执行期间 LR 不会被 BL 改写,回程票一直揣在口袋里,直接 RET 即可。编译器对叶子函数会自动省略保存 LR 的开销,这也是常见优化之一。

为何常见:学习者只见过 main→A→B 的嵌套示例,没意识到“叶子”这一特殊分支,便把所有函数一概而论。

误区“AArch64 有 31 个通用寄存器,局部变量放寄存器就够了,栈帧其实没啥用。”看到栈帧里的局部变量区,觉得纯属浪费。

正解寄存器数量有限,且 callee-saved 寄存器要承担跨函数保存职责。当函数局部变量较多、或发生地址取用(&x)时,编译器必须把变量“溢出”(spill)到栈帧的局部变量区。这在递归场景尤其明显——每层递归的局部变量都要独立栈帧副本。

为何常见:写小函数时变量确实能全待在寄存器里,让人低估了栈帧的必要性;一遇到大数组局部变量和递归,整套寄存器方案立刻失效。

面试题

Q1. AAPCS64 中栈指针 SP 的增长方向是?

单选题 · easy

  • A.向低地址方向增长← 正确答案
  • B.向高地址方向增长
  • C.随机跳跃式分配
  • D.由系统动态调整

答案:A。AArch64 的栈是向下生长的,调用函数时 SP 减小,指向低地址。B 是堆的典型方向,C 和 D 不符合任何栈式内存管理。记住“上台叠布景”——新布景总摆在更低的位置。

Q2. LR(链接寄存器)的主要用途是?

单选题 · easy

  • A.保存函数返回地址← 正确答案
  • B.保存全局变量地址
  • C.保存栈指针备份
  • D.保存帧指针备份

答案:A。BL 指令执行时硬件把 PC+4 写入 LR,函数返回用 RET 跳回该地址。它像演员的回程票,不存全局变量、也不备份 SP/FP,所以 B/C/D 均不对。

Q3. 叶子函数(Leaf Function)的特点是?

单选题 · easy

  • A.不再调用其他函数← 正确答案
  • B.使用大量局部变量
  • C.必须保存所有寄存器
  • D.不允许访问栈内存

答案:A。叶子函数是“独角戏”——函数体内没有对其他函数的调用,因此 LR 不会被覆盖,无需压栈保存。局部变量大小不影响“叶子”判定,更无须保存所有寄存器。

Q4. 非叶子函数中 LR 需要被压栈保存,原因是?

单选题 · mid

  • A.后续调用会覆盖它← 正确答案
  • B.存储空间太过狭小
  • C.在启动后即失效
  • D.便于调试器识别

答案:A。funcA 调用 funcB 时,BL 会改写 LR 为“funcA 中下一条指令地址”,原本保存的 main 回程地址就被冲掉。把 LR 压栈寄存,返回时再取回,如同把回程票寄存到服务台。C/D 与硬件行为无关。

Q5. FP(帧指针)指向栈帧的哪一端?

单选题 · mid

  • A.低地址端(帧顶)
  • B.高地址端(帧底)← 正确答案
  • C.帧内局部变量中点
  • D.任意固定偏移处

答案:B。FP 指向栈帧底部,即入口调整 SP 后的位置,局部变量与保存寄存器都相对它做偏移寻址。若把栈想像成布景叠层,FP 就是画在布景底边的红色边界柱,不是顶边也不是中点。

Q6. AAPCS64 要求栈指针 SP 在函数入口处的对齐方式是?

单选题 · mid

  • A.16 字节对齐← 正确答案
  • B.8 字节对齐
  • C.4 字节对齐
  • D.1 字节对齐

答案:A。规范强制栈指针保持 16 字节对齐,以保证 SIMD 访存等操作的效率与正确性。若需求空间不是 16 的倍数,编译器会用取整指令向上补齐,正如布景支架必须卡在标准滑轨上。

Q7. 关于被调用者保存寄存器(Callee-saved),正确的是?

单选题 · hard

  • A.由被调用者负责恢复原值← 正确答案
  • B.由调用者负责保存它们
  • C.无需保存可直接使用
  • D.每次调用自动清零

答案:A。x19–x29 属于被调用者保存寄存器,函数若使用必须压栈并在返回前恢复,如同借道具要原样归还。调用者不需管理这些寄存器,硬件也不会自动清零它们,故 B/C/D 错。

Q8. 调试器利用 FP 链回溯调用栈时,关键在于?

单选题 · extreme

  • A.沿当前FP逐层读取← 正确答案
  • B.沿当前SP逐层读取
  • C.依靠全局变量获取
  • D.凭借LR反推信息

答案:A。回溯的核心是 FP_{i+1} = [FP_i]:从当前 FP 指向的地址读出的值恰是上一层 FP,沿链上溯直到 main。SP 指向栈顶不含链信息,全局变量与 LR 也无法独立还原整个调用链。

Q9. 一个标准 AAPCS64 栈帧中通常包含哪些区域?

多选题 · mid

  • A.调用者的参数区← 正确答案
  • B.保存的寄存器区← 正确答案
  • C.局部变量区← 正确答案
  • D.全局常量区

答案:ABC。栈帧由参数传递区、保存寄存器区、局部变量区三块构成,对应“传词窗口、道具寄存架、舞台道具”。全局常量区在只读数据段而非栈上,所以 D 不选。

Q10. 栈展开(Stack Unwinding)技术常用于哪些场景?

多选题 · hard

  • A.异常处理与捕获← 正确答案
  • B.调试器打印调用栈← 正确答案
  • C.提高指令执行速度
  • D.编译器优化寄存器分配

答案:AB。异常处理需要沿 FP 链/CFI 逐层释放栈帧(A),调试器 backtrace 也要靠栈展开还原调用序列(B)。栈展开是“事后追溯”,不影响指令执行速度,也与编译期寄存器分配无关,C/D 错。