想象一座剧场:一位喜剧演员接到剧务的呼叫“该你上场了!”,他快步走到后台,先把手机、钱包、外套塞进编号储物柜,再在心里默念一遍散场后要回的房间号,这才掀帘登台。散场后,他取回所有物品,顺着来时的路退回休息室——一切恢复原样。在程序的世界里,每一次函数调用都上演着同一幕:函数一进门,就有一段“上台准备”的固定动作——把重要的东西(比如回家的导航卡、向剧场借的道具)放进自己的储物柜,再划出一块私人空间;函数要离开时,再做一套完全相反的“谢幕整理”——取回东西、撤销空间、按导航卡原路返回。这组“上台/下台”的固定仪式,就是 ARM 芯片上函数调用规范(AAPCS64)里的函数序言与函数尾声。看懂它,你就能在一堆机器码里认出每个函数的开头和结尾,也能明白栈是怎么一层层搭起来又被拆掉的。

岗位关联

对 AI 从业者而言,掌握序言/尾声机制意味着能读懂编译器产出与二进制程序的结构规律,避免“看反汇编像看天书”,在模型部署优化、系统安全分析时拥有底层视角。

目录
  1. 可视化演示
  2. 序言职责:上台前的三件套
  3. 尾声职责:谢幕后的还原仪式
  4. 寄存器分工:谁的东西谁负责
  5. STP/LDP 配对:双手存取的艺术
  6. 栈指针调整:16 字节的硬规矩
  7. PAC 指针验证:给返回地址盖上防伪章
  8. 反汇编识别:看监控找上台与下台
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

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

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

序言职责:上台前的三件套

演员被剧务喊“上场”后,不会直接冲上舞台——他先确认三件事:把手机和钱包塞进后台储物柜、在心里记住“散场后回休息室”的路线、再往道具间一探确认自己的戏服已挂好。这三步做完,他才敢掀帘登台。在 AAPCS64 的世界里,函数被调用的那一刻同样要做这三件事,把它们合起来就叫函数序言。

序言的第一要务是保存链接寄存器 x30(也叫 LR)。LR 里存的是“演出结束后该回哪一行代码”的地址,相当于演员记下的回家路线;函数一旦调用别的函数,LR 就会被新地址覆盖,所以必须第一时间存起来。第二要务是保存“被调用者保存寄存器”(x19-x28 中实际用到的那几个)——它们是演员从后台借出来的道具,上台前登记在案,下台要原样归还。第三要务是调整栈指针 SP,为自己搭出一块私人的“化妆间+储物区”,也就是栈帧。

整个序言在汇编里看起来就像一次“批量打包 + 腾地方”:先用 stp 把 LR 与帧指针 x29 成对压栈,再对实际用到的 x19-x28 逐个存储,最后用 sub sp 一次性分配局部变量空间。AAPCS64 允许编译器按需只保存用到的寄存器,不必全存,因此序言的长短直接反映了函数体对寄存器的消耗。

三件事做完,演员的“上台前”彻底准备好:导航存好了,道具登记好了,私人空间也划出来了。这时才进入函数体——也就是舞台上的正式表演。

序言:SPnew=SPold−F,[SPnew+δi]←{LR, x19,…,x28}\displaystyle \text{序言:}\quad SP_{new} = SP_{old} - F,\qquad [SP_{new} + \delta_{i}] \leftarrow \{LR,\ x_{19},\dots,x_{28}\}
通俗说明:这段公式是“先向下移动储物柜标尺腾出格子,再把 LR 和需要归还的寄存器依次放进对应格位”的数学写法。
  1. 下调 SP 预留空间把栈指针向下移动 F 字节,相当于演员先把储物柜标尺往下挪,腾出整块新区域
    SPnew=SPold−FSP_{new} = SP_{old} - F
  2. 保存链接寄存器把 LR(返回地址)存入栈帧中某个确定偏移处,保证之后无论怎么调用别人都能找回回家的路
    [SPnew+δLR]←LR[SP_{new} + \delta_{LR}] \leftarrow LR
  3. 保存被调用者保存寄存器将 x19 到 x28 中函数实际使用到的寄存器,按约定偏移依次写入栈帧
    [SPnew+δi]←xi,i∈{19,…,28}[SP_{new} + \delta_{i}] \leftarrow x_{i},\quad i \in \{19,\dots,28\}

尾声职责:谢幕后的还原仪式

演出结束不是放下话筒就走。演员先回到后台储物柜,把借来的道具一件件按登记清单放回原位,取回自己的手机钱包,再顺着来时记下的路线走出剧场后门。AAPCS64 的函数尾声就是这场还原仪式,它必须和序言“镜像对称”。

尾声做的事与序言完全相反:先把栈帧里保存的寄存器逆序弹回原寄存器(用 ldp 成对恢复),把 LR 从栈里取回,然后把栈指针 SP 加回原来的值,最后执行 ret 指令跳转到 LR 指向的地址继续执行。逆序是一条铁律:序言先存谁,尾声就后取谁。

为什么必须逆序?因为寄存器保存和恢复之间可能存在依赖关系,而且 STP 往往把 LR 与其他寄存器配对地放在一起;逆序恢复能保证任一时刻栈内容都不会被误覆盖。实际反汇编里尾声通常只有三四条指令:ldp x29,x30,[sp],#32 再跟一条 ret——简洁、对称、可预测。

于是,“上台前存了什么,下台后就原样取回什么”,演员与剧场两不相欠。这正是 ABI 的意义——调用者与被调用者之间不需要任何事先商量,凭这套约定就能无缝交接。

尾声:xi←[SPnew+δi],LR←[SPnew+δLR],SPold=SPnew+F\displaystyle \text{尾声:}\quad x_{i} \leftarrow [SP_{new} + \delta_{i}],\qquad LR \leftarrow [SP_{new} + \delta_{LR}],\qquad SP_{old} = SP_{new} + F
通俗说明:尾声就是序言的“倒放”——依次从格位里取出道具、取回导航卡,再把标尺移回原来的位置。
  1. 逆序恢复寄存器按序言保存顺序的倒序,把 x19-x28 从栈中弹回寄存器,如同按清单逆序归还道具
    xi←[SPnew+δi]x_{i} \leftarrow [SP_{new} + \delta_{i}]
  2. 恢复链接寄存器 LR把保存的返回地址取回 LR,仿佛重新拿到回家的导航
    LR←[SPnew+δLR]LR \leftarrow [SP_{new} + \delta_{LR}]
  3. 撤销栈帧并返回将 SP 加回 F,使栈指针回到函数入口的值,随后 ret 跳转到 LR 地址
    SPold=SPnew+F,PC←LRSP_{old} = SP_{new} + F,\quad PC \leftarrow LR

寄存器分工:谁的东西谁负责

剧场后台有条不成文的规矩:化妆台上的口红、假发属于“演员自带物品”,离开时必须自己带走;而剧场提供的道具枪、摩托车则登记后由使用者负责归还。AAPCS64 的寄存器也分两个“产权”阵营:调用者保存寄存器与被调用者保存寄存器。

x0-x18 是“自带物品”——调用者保存寄存器(caller-saved)。调用者如果要保留它们的值,必须自己压栈,被调用者爱怎么改就怎么改。x19-x28 属于“剧场道具”——被调用者保存寄存器(callee-saved)。被调用者只要用到了它们,就必须在序言中保存、在尾声中恢复,保证回到调用者眼前时“原封不动”。

另外还有两个特殊角色:x29(帧指针 FP)与 x30(链接寄存器 LR)。x29 常被当作“舞台侧幕的挂钩”——把上一帧的 SP 串成链表,方便调试器回溯;x30 则存放返回地址。两者都属于被调用者必须归还的名单。这套分工把“保存责任”清晰地画在纸上,函数之间才能各自为政、互不打扰。

明确了产权,序言里“该存谁”就不再靠猜:凡是函数体写过的被调用者保存寄存器,一个不少地存;没碰过的,一个不多地留。这套规则直接决定了序言尾声的肥瘦。

∀ x∈{x19,…,x28,x29,x30}:Out(x)=In(x)\displaystyle \forall\,x \in \{x_{19},\dots,x_{28},x_{29},x_{30}\}:\quad \mathrm{Out}(x) = \mathrm{In}(x)
通俗说明:“出函数时的值必须等于进函数时的值”,这就是被调用者保存规则的数学表达。
  1. 被调用者保存集合先圈定哪些寄存器“进出必须一致”,包括 x19-x28、帧指针 x29、链接寄存器 x30
    x∈{x19,…,x28,x29,x30}x \in \{x_{19},\dots,x_{28},x_{29},x_{30}\}
  2. 进出不变约束函数执行前与返回后,这些寄存器的值必须完全相同,即“归还的道具一件都不能少”
    Out(x)=In(x)\mathrm{Out}(x) = \mathrm{In}(x)
  3. 调用者保存集合不在约束内x0-x18 不在此列,若调用者需要保留它们的值就必须自己保存
    x∈{x0,…,x18}: 无此约束x \in \{x_{0},\dots,x_{18}\}:\ \text{无此约束}

STP/LDP 配对:双手存取的艺术

收拾储物柜时,细心的演员会发现:把手机和钱包抓在两只手里,一次就能同时塞进同一层格子,比一件一件放快一倍。ARM 的 STP/LDP 就是这双“左右开弓的手”——一条指令同时存/取两个 64 位寄存器。

STP(Store Pair)把两个寄存器连续写入栈上相邻的两个 8 字节槽位;LDP(Load Pair)把相邻槽位成对弹回寄存器。在序言尾声里,它俩几乎总是成对出现:stp x29,x30,[sp,#-32]! 负责压入,ldp x29,x30,[sp],#32 负责弹出。注意感叹号的位置:前变址(! 表示先更新 SP 再使用)与后变址(结束后再更新 SP)的书写顺序恰恰相反——这也是初学者最爱看反的一对。

配对还有偏移规范:STP 的偏移必须是 8 的倍数,且落在 ±512 字节的可编码窗口内(7 位有符号立即数按 3 位移位);LDP 使用的偏移必须与对应的 STP 完全一致,否则恢复的就不是当初保存的位置。编译器总是把这两个偏移对齐,让“存入”与“取出”成为严格的镜像。

双手存取的另一个好处是“原子感”:单条指令完成两个寄存器移交,既省取指带宽,又让栈帧的打包与解包天然成对。理解 STP/LDP,就能一眼看出反汇编里“舞台道具正在进出储物柜”。

STP x19,x20, [SP,#−16]!  ⟷  LDP x19,x20, [SP],#16\displaystyle \text{STP}\ x_{19},x_{20},\ [SP,\#-16]! \;\longleftrightarrow\; \text{LDP}\ x_{19},x_{20},\ [SP],\#16
通俗说明:左边是“双手把 x19、x20 同时放进新腾出的格子并把标尺下移 16”,右边是“把格子里的东西同时取回并把标尺上移 16”,一来一回完全对称。
  1. 成对压栈 STP把 x19 存入低 8 字节、x20 存入高 8 字节,偏移 -16 且带 ! 表示先减 SP 再写入
    [SP−16]←x19,[SP−8]←x20,SP←SP−16[SP-16] \leftarrow x_{19},\quad [SP-8] \leftarrow x_{20},\quad SP \leftarrow SP-16
  2. 成对弹栈 LDP将低地址槽位载入 x19、高地址载入 x20,偏移 #16 不带 ! 表示先载入再回写 SP
    x19←[SP],x20←[SP+8],SP←SP+16x_{19} \leftarrow [SP],\quad x_{20} \leftarrow [SP+8],\quad SP \leftarrow SP+16
  3. 偏移编码约束立即数必须是 8 的倍数,范围限制在 7 位有符号位移可表示的窗口内
    imm=8×n,n∈[−64,63]\mathrm{imm} = 8 \times n,\quad n \in [-64,63]

栈指针调整:16 字节的硬规矩

剧场储物柜的格子由物业统一规格:每层柜高固定 16 厘米,谁也别想塞一个 15 厘米高的夹层进去。AAPCS64 对栈指针 SP 也立了这么一条硬规矩:SP 的值必须始终保持 16 字节对齐。

因为 AArch64 的加载存储指令和某些 SIMD、原子操作要求地址对齐,AAPCS64 干脆要求 SP 从函数入口到出口始终 16 字节对齐。于是序言里分配栈帧时,帧大小必须是 16 的倍数;明明只需要 20 字节局部空间,编译器也会“向上取整”到 32 字节。

栈向下增长,序言用一条 sub sp, sp, #F 一次性分配整个帧(而不是多次小减),这样既能保证对齐,又能让调试器与栈回溯工具看到清晰的“帧边界”;尾声则用 add sp, sp, #F 一条指令整体撤销。

这条 16 字节规矩看起来浪费了几个字节,却让编译器、调试器和操作系统都省心——栈上任何位置的访问都天然对齐。正如剧场统一柜格,看似死板,实则让整个后台井然有序。

frame_size=16×⌈B16⌉,SPafter≡SPbefore(mod16)\displaystyle \text{frame\_size} = 16 \times \left\lceil \frac{B}{16} \right\rceil,\qquad SP_{after} \equiv SP_{before}\pmod{16}
通俗说明:需要 B 字节就把柜子租成 16 的整数倍;进函数前后的 SP 除以 16 余数相同,保证标尺始终踩在物业划好的刻度上。
  1. 向上取整到 16 字节把实际需要的字节数 B 补足到 16 的倍数,得到正式分配的帧大小
    F=16×⌈B16⌉F = 16 \times \left\lceil \frac{B}{16} \right\rceil
  2. 对齐不变式序言减帧后、尾声加帧前,SP 对 16 取模的值始终保持不变
    SPafter≡SPbefore(mod16)SP_{after} \equiv SP_{before}\pmod{16}
  3. 一次性分配与撤销序言用一条 SUB 建帧,尾声用一条 ADD 销毁,形成“入口唯一、出口唯一”的对称结构
    SP←SP−F(入口),SP←SP+F(出口)SP \leftarrow SP - F \quad(\text{入口}),\qquad SP \leftarrow SP + F \quad(\text{出口})

PAC 指针验证:给返回地址盖上防伪章

剧场保安最近抓到一个偷换演员通行证的惯犯:他趁演员转身时,把通行证上的休息室房间号改成自己的宿舍。于是剧场开始给每张通行证盖一枚特制防伪章,进门时先验章再放行。ARM 的 PAC(Pointer Authentication Code,指针认证码)干的就是这事——只不过“通行证”是 LR 里的返回地址。

在序言里,启用 PAC 的函数会先执行 PACIASP 指令:用硬件密钥和当前 SP 为上下文,给 LR 里的返回地址计算一个紧凑的完整性签名,把签名“嵌入”地址的高位字段;在尾声里,AUTIASP 指令对签名进行验证,通过才把地址还原给 LR 去 ret。章盖得对不对,一验便知。

若攻击者篡改返回地址,AUTIASP 验证出的地址会变成毫无意义的垃圾值,ret 跳转时直接触发异常,程序当场停工而不是悄然被劫持。PACIASP/AUTIASP 在序言尾声中的位置通常是:序言最先执行签名,尾声在恢复 LR 前执行验证——这给“返回地址被覆写”这类经典攻击上了一把硬锁。

注意,PAC 是“防篡改”而非“防偷看”的加密。它保护的是返回地址的完整性,相当于通行证上的防伪章,而不是把通行证内容加密成密文。理解了这层,你就能在反汇编里识别哪些函数启用了防护、哪些还在裸奔。

PACIASP: LR′←PACIA(LR, SP),AUTIASP: LR←AUTIA(LR′, SP)\displaystyle \text{PACIASP: } LR' \leftarrow \mathrm{PACIA}(LR,\ SP),\qquad \text{AUTIASP: } LR \leftarrow \mathrm{AUTIA}(LR',\ SP)
通俗说明:序言给返回地址盖“防伪章”(PACIA 签名),尾声验章(AUTIA 验证)——章被破坏,地址就立刻爆成一串无效字符,让劫持计划瞬间露馅。
  1. 序言签名PACIASP 以 LR 原文与当前 SP 为输入,用硬件密钥算出认证码并嵌回地址得到 LR'
    LR′←PACIA(LR, SP)LR' \leftarrow \mathrm{PACIA}(LR,\ SP)
  2. 尾声验证AUTIASP 用同样的 SP 上下文重新计算并比对,验证通过则把干净地址写回 LR
    LR←AUTIA(LR′, SP)LR \leftarrow \mathrm{AUTIA}(LR',\ SP)
  3. 失败即异常验证失败时返回被“污染”的地址,后续 ret 取指触发故障,阻止控制流劫持
    fail⇒PC←无效值⇒异常\mathrm{fail} \Rightarrow PC \leftarrow \text{无效值} \Rightarrow \text{异常}

反汇编识别:看监控找上台与下台

有了前面几节的“舞台知识”,你站在监控室看一段反汇编,就像看演员的入场离场录像:谁在开场三秒内掏柜子钥匙(STP/LDP),谁在结束前整理衣冠(恢复寄存器),一眼就能认出“上台”与“下台”的完整套路。

函数边界的识别规律非常机械:入口处若出现 stp x29,x30,[sp,#-N]! 或 sub sp, sp, #F,基本可以断定这里是序言;出口处若出现 ldp x29,x30,[sp],#N、add sp, sp, #F、ret 这样的组合,这里就是尾声。“入口唯一”意味着函数起点认准第一条 stp/sub;“出口可能多个”意味着 ret 可能出现多次,但每个出口前都有同一套恢复指令。

也要注意特例:叶子函数(不调用其他函数且不改被调用者保存寄存器)可以完全没有序言尾声,直接一个 ret 结束;启用 PAC 的函数,入口第一条往往是 paciasp,出口 ldp 前会插一条 autiasp。把这些模式写成规则,就能让机器自动“切分”出一个个函数的花名册。

掌握这套识别术,反汇编在你眼里就不再是乱码瀑布,而是一幕幕“登场、表演、谢幕”的舞台剧。辨识函数边界、重建栈帧、判断是否启用 PAC,正是性能分析与安全审计的第一块敲门砖。

start(f): instr0∈{STP, SUB sp},end(f): instrlast=RET\displaystyle \text{start}(f):\ \text{instr}_{0} \in \{\text{STP},\ \text{SUB}\ sp\},\qquad \text{end}(f):\ \text{instr}_{\text{last}} = \text{RET}
通俗说明:一个函数的“门牌号”由第一条指令决定——是 STP 或 SUB SP 就是进门;最后一个 RET 就是出门。特例是无序言无尾声的叶子函数。
  1. 入口特征函数第一条指令出现在 STP 压栈或 SUB SP 减栈集合中时,判定为序言边界
    instr0∈{STP, SUB sp}\text{instr}_{0} \in \{\text{STP},\ \text{SUB}\ sp\}
  2. 出口特征函数最后一条指令是 RET,且之前通常跟随着 LDP 或 ADD SP 恢复序列
    instrlast=RET\text{instr}_{\text{last}} = \text{RET}
  3. 对称性校验把 LDP 偏移与 STP 偏移、ADD SP 增量与 SUB SP 减量两两比对,完全一致才判定栈帧完整
    offldp=offstp,add_amt=sub_amt\text{off}_{ldp} = \text{off}_{stp},\quad \text{add\_amt} = \text{sub\_amt}

AI 应用场景

场景 1:编译器与性能优化工程师的栈帧瘦身

阶段:性能优化

解决的问题:传统方式下,排查“为什么这个函数栈帧这么大”要靠人肉翻汇编,逐条核对每一条 stp/ldp 和 sub sp 的位移;AI 辅助的静态分析能自动扫描函数序言/尾声,统计保存的寄存器数量与帧大小,识别出“过度保存”或“非对齐分配”的浪费点。

具体案例:某车载推理引擎的编译器工程师用脚本对所有热函数做序言特征提取,发现一个高频小函数的序言保存了 6 个 x19-x28,但函数体实际只用了 2 个;按 ABI 规则修剪后,该函数栈帧从 64 字节降到 48 字节,千万次调用累计节省明显。

场景 2:固件安全审计员的 PAC 覆盖率检查

阶段:安全审计

解决的问题:栈溢出攻击的经典目标是覆写 LR 劫持控制流。传统人工审计面对上万函数无力逐一看清哪些序言/尾声配对了 PACIASP/AUTIASP;AI 模型在反汇编上做模式匹配,自动标注“有栈帧但未启用 PAC”的函数清单,供团队优先补防。

具体案例:某 IoT 固件审计中,用序言/尾声分类器跑完全部二进制,发现 73 个可被外部输入触达且未签名返回地址的函数;修复团队基于清单给这些函数补上 PAC 指令,把可利用面大幅收窄。

Python 演示

python56 行
class Stack:    """Simulate AArch64 stack memory with dict."""     def __init__(self, base=1024):        self.base = base        self.sp = base        self.mem = {}     def sub(self, n):        self.sp -= n     def add(self, n):        self.sp += n     def store(self, reg, off):        self.mem[self.sp + off] = reg     def load(self, off):        return self.mem[self.sp + off]  def aapcs64_prologue(stack, lr, callee_saved, frame_size):    """Prologue: save LR and callee-saved regs, then alloc frame."""    saved = [lr] + callee_saved    stack.sub(len(saved) * 8)      # 为保存区腾出空间, 等价于序言的连续压栈    for i, r in enumerate(saved):        stack.store(r, i * 8)      # 逐个写入保存区    stack.sub(frame_size)          # 再分配局部变量区  def aapcs64_epilogue(stack, lr, callee_saved, frame_size):    """Epilogue: dealloc frame, restore regs, SP back to entry."""    stack.add(frame_size)          # 先销毁局部变量区    saved = [lr] + callee_saved    restored = [stack.load(i * 8) for i in range(len(saved))]    stack.add(len(saved) * 8)      # 再销毁保存区    return restored  if __name__ == "__main__":    lr = 0x4007D0                  # 返回地址, 类比演员的导航卡    callee_saved = ["x19", "x20", "x21"]    frame_size = 32                # 局部变量区, 满足 16 字节对齐     st = Stack()    entry_sp = st.sp     aapcs64_prologue(st, lr, callee_saved, frame_size)    print("SP after prologue =", st.sp, ", moved down", entry_sp - st.sp, "bytes")     restored = aapcs64_epilogue(st, lr, callee_saved, frame_size)     assert restored == [lr] + callee_saved, "registers must be restored"    assert st.sp == entry_sp, "SP must return to entry value"    print("epilogue restored:", restored)    print("SP recovered =", st.sp)
目的:用约 50 行 Python 模拟 AAPCS64 序言/尾声的“保存-恢复”全过程,验证 SP 与寄存器在函数进出前后完全一致。
  1. stack.sub(len(saved) * 8)先把储物柜标尺下移,为 LR 和被调用者保存寄存器腾出格子
  2. for i, r in enumerate(saved): stack.store(r, i * 8)逐个把寄存器按偏移存入栈帧保存区
  3. stack.sub(frame_size)再划出局部变量区,相当于演员搭好专用化妆间
  4. restored = [stack.load(i * 8) for i in range(len(saved))]从保存区原样取回所有寄存器的值
  5. assert restored == [lr] + callee_saved校验“借用道具全部归还”
  6. assert st.sp == entry_sp校验 SP 回到入口值,证明序言与尾声严格对称

常见误区

误区既然是上台前的准备,那把所有寄存器都存进栈里最保险,反正编译器就是这么干的吧?

正解AAPCS64 的分工很明确:x0-x18 是调用者保存,谁污染谁负责;被调用者只需保存自己实际用到且属于 x19-x28 的寄存器,以及 LR。反汇编里常见到 stp x29,x30 或少量 x19-x28,是因为编译器做过活跃变量分析,只保存必要集合。多存只是浪费栈空间和访存带宽。

为何常见:新手面对编译器生成的样板指令,误以为固定模板就是“全保存”,没注意到保存集合因函数而异。

误区一条 `STP x19,x20` 反正等价于先存 x19 再存 x20,分开写也没区别。

正解STP/LDP 是真正的“打包”指令:单条指令、一次访存、微架构上可合并,还能用前/后变址写回一次性完成任务。而且它们有配对规范——低地址放第一个操作数;尾声里 LDP 的偏移必须与序言中 STP 的偏移配对一致。两条 STR 无法保证“成对原子性”与同一条写回语义。

为何常见:人们习惯用已知的简单指令去解释复杂指令,忽略了编码格式和微架构的差异。

误区栈向下长,我序言里随便 `sub sp, #20` 也一样干活,差几个字节无所谓。

正解AAPCS64 强制 SP 在任何时刻都是 16 字节对齐,对齐违规的非法访问会直接触发对齐异常而不是静默出错;编译器的 frame_size 永远是 16 的倍数。这也是为什么减 20 字节的“随手优化”会在使用 SIMD 或原子指令时让程序崩溃。

为何常见:先入为主的 x86 栈对齐要求较宽松,忽略了 AArch64 对 SP 的硬性对齐规则。

误区PACIASP 把 LR“加密”了,攻击者就算改写返回地址也读不出来,绝对安全。

正解PAC 不是加密而是 MAC 完整性校验:PACIA 用密钥和上下文(SP)对地址算一个短签名,AUTIA 验证签名,失败时会把返回地址污染成错误值触发异常。它防“篡改”但不防“重放”,签名可能被侧信道或任意签名预言机攻击,不能当作密码学加密来使用。

为何常见:“签名”与“加密”都涉及密钥,初学者容易把完整性保护误当成机密性保护。

面试题

Q1. 函数序言中最先需要保存、防止函数“迷路”回不了调用者的寄存器是哪一个?

单选题 · easy

  • A.x0 参数寄存器
  • B.x19 被调用者保存
  • C.LR 链接寄存器← 正确答案
  • D.SP 栈指针寄存器

答案:C。序言首先要保存 LR,因为函数一旦调用别人 LR 就会被覆盖,等于演员丢了回家导航;x0 是调用者保存,x19 按需保存,SP 是“储物标尺”本身谈不上保存到调用者处。

Q2. AAPCS64 规定由被调用者负责保存与恢复的是哪一组寄存器?

单选题 · easy

  • A.x0~x18 调用者保存
  • B.x19~x28 被调用者保存← 正确答案
  • C.仅 x29 帧指针寄存器
  • D.仅 x30 链接寄存器

答案:B。x19-x28 与 x29、x30 都属于被调用者必须“原样归还”的名单;x0-x18 是调用者自己的责任,选项 C/D 只说对了一半,忽略了被调用者还要维护 x19-x28。

Q3. 指令 stp x29, x30, [sp, #-32]! 中感叹号的含义是?

单选题 · easy

  • A.存储后把新地址写回 SP← 正确答案
  • B.存储前交换两个寄存器
  • C.将栈指针清零再存储
  • D.表示条件执行的指令

答案:A。这是前变址写回:先 SP-32 得到新地址,再用新地址做基址存储,最后把新地址写回 SP。相当于先挪标尺再放物品,是序言中最经典的写法。

Q4. 下列关于 AAPCS64 栈指针对齐的说法,正确的是?

单选题 · mid

  • A.SP 只需在函数入口处对齐
  • B.SP 任何时刻都须 16 字节对齐← 正确答案
  • C.SP 只在访问内存时需对齐
  • D.SP 只需保持在 8 字节边界

答案:B。AAPCS64 要求 SP 在所有时刻保持 16 字节对齐,违规访问会触发对齐异常;选项 A/C/D 都放宽了这条硬规矩,与规范不符。

Q5. 要确认一段反汇编属于“函数序言”而不是“尾声”,最合理的判据是?

单选题 · mid

  • A.包含恢复寄存器的 LDP 指令
  • B.包含建立栈帧的 SUB SP 操作← 正确答案
  • C.以 RET 指令作为整个序列结尾
  • D.以 ADD SP 指令恢复栈指针

答案:B。序言的任务是保存与建栈,SUB SP 分配栈帧是它的标志性动作;LDP、RET、ADD SP 都是尾声特征。认准“减栈建帧”就能把两者区分开。

Q6. PAC 指针认证中,PACIASP 指令真正起到的作用是?

单选题 · mid

  • A.对 LR 签名以便检测篡改← 正确答案
  • B.对栈上数据做加密保护
  • C.把 LR 清零防止地址泄漏
  • D.将 SP 与 LR 做一次异或

答案:A。PACIASP 是完整性签名而非加密,它用 SP 做上下文给 LR 盖防伪章;B/D 混淆了加密与签名,C 若清零 LR 会让函数彻底回不去。

Q7. 对于一条 ldp x29, x30, [sp], #16 尾声指令,下列说法正确的是?

单选题 · hard

  • A.先恢复寄存器再回写栈指针← 正确答案
  • B.先调整栈指针再恢复寄存器
  • C.同时完成保存现场与建栈帧
  • D.只能用于恢复 x29 一个寄存器

答案:A。这是后变址 LDP:先按当前 SP 载入 x29/x30,再 SP+=16。顺序绝不能反过来,这正是它与 STP 前变址的镜像关系;D 错在它明明恢复两个寄存器。

Q8. 关于函数尾声的“现场还原”要求,下列哪些说法正确?

多选题 · mid

  • A.恢复被调用者保存寄存器← 正确答案
  • B.由调用者负责恢复 LR 值
  • C.将 SP 恢复为入口时的值← 正确答案
  • D.清空栈上的所有历史数据

答案:AC。尾声要恢复被调用者保存寄存器并把 SP 还原到入口值;LR 的恢复正是尾声自己干的活,不会丢给调用者;清空整块栈既不必要也不可能,栈数据只是不再被引用。

Q9. 识别“没有序言尾声”的叶子函数时,哪些观察是合理的?

多选题 · hard

  • A.既没有 STP 也没有 LDP 配对← 正确答案
  • B.函数末尾使用 BR 而非 RET
  • C.没有修改被调用者保存寄存器← 正确答案
  • D.函数体内必然没有任何局部变量

答案:AC。叶子函数不调用别人,若也不碰被调用者保存寄存器,就不需要保存和恢复,自然没有 STP/LDP 配对;“没有局部变量”太绝对,局部数据可以放在寄存器里;用 BR 做尾调用并不等于没有序言。

Q10. 关于函数序言与尾声的配对规范,下列说法正确的是?

多选题 · extreme

  • A.LDP 恢复顺序与 STP 严格相反← 正确答案
  • B.STP 与 LDP 的偏移量应当相同← 正确答案
  • C.尾声可以跳过恢复 LR 直接返回
  • D.SP 的减量与增大量必须相等← 正确答案

答案:ABD。恢复按保存的逆序、LDP 与 STP 偏移一致、SP 一减一加相等,三者共同保证栈帧严格对称;跳过恢复 LR 直接 ret 会跳到被破坏的地址,只有故意制造异常时才这么做。