想象一家公司把办公楼盖成了「倒着长」的样子:从地面(高地址)往地心(低地址)一层层往下挖,楼层越深,房号越小。每个函数就是某一层楼里开会的人,他们摆开桌椅、摊开文件所占用的那块空间,叫栈帧;脚下站着的楼层地板,就是RSP(栈指针);身后用来量尺寸的承重墙基准线,是RBP(帧指针)。会议开完,人走楼空,地板回升,一切归还。 SysV ABI 就是这栋楼的「物业管理条例」。条例规定:会议室的桌面必须卡在 16 厘米的消防地砖网格线上(RSP 16 字节对齐);每层楼地板正下方有 128 厘米深的「暗格」(红区),可以随手塞临时物品,物业大扫除时保证不碰它——但如果有别的部门进你的楼层开会(函数调用),新来的同事会直接踩在暗格里,你的东西就被覆盖了。读懂这套楼层规则,你就同时读懂了汇编代码、编译器输出和崩溃时的栈回溯。

岗位关联

对 AI 从业者而言,这是理解编译器后端、调试模型崩溃、排查性能热点绕不开的 ABI 底层知识,也是系统方向面试的高频考点。

目录
  1. 可视化演示
  2. 栈帧构成:会议桌上的三样东西
  3. 入栈出栈:升降梯的楼层管理
  4. RSP对齐:消防地砖上的会议桌
  5. 红区定义:地板下的免费暗格
  6. 叶函数优化:独居办公室的自由
  7. 信号安全:物业大扫除的承诺
  8. 调试验证:监控探头下的楼层检查
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

\u901a\u8fc7\u4e09\u9636\u6bb5\u4ea4\u4e92\u5c55\u793a\u6808\u5e27\u7ed3\u6784\u4e0e\u51fd\u6570\u5e8f\u8a00/\u5c3e\u58f0\u3001\u7ea2\u533a\u514d\u8c03\u6574\u673a\u5236\u4e0e16\u5b57\u8282\u6808\u5bf9\u9f50\u89c4\u5219\uff0c\u8986\u76d6\u53f6\u51fd\u6570\u4f18\u5316\u4e0eABI\u7ea6\u5b9a\u3002

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

\u6808\u5e27\u7ed3\u6784\u4e0e\u5e8f\u8a00\u5c3e\u58f0 \u8fd4\u56de\u5730\u5740 \u4fdd\u5b58RBP push rbp;mov rbp,rsp \u5e8f\u8a00\u5efa\u7acb\u6808\u5e27\u57fa\u5740

栈帧构成:会议桌上的三样东西

公司地下办公楼里,每个部门开会都会在自己的楼层里摆开一张「会议桌」。桌上固定有三样东西:门边的「来路指示牌」——记录会议结束该回哪一层,这是返回地址;墙角的「旧承重墙标记」——记录上一层楼的基准线,这是保存的 RBP;桌面中央的草稿纸与便签——函数的局部变量。如果参会人太多,桌上放不下,第 7 个以后的名牌就得摞到桌边的走廊上,这就是参数溢出区。

在汇编层面,函数的开场白(prologue)就是按固定顺序布置这张桌:先把返回地址交给 call 指令压栈,然后 push rbp 把上一层的承重墙标记贴到墙上,再 mov rbp, rsp 给这层楼确立新基准,最后 sub rsp, N 为局部变量腾出桌面。这四步做完,一个栈帧就搭起来了。

栈帧的总开销可以精确计算:无论局部变量多小,返回地址和保存的 RBP 各占 8 字节,局部变量区还要按 16 字节规则补齐。

会议开完,leave 指令负责收桌:先恢复旧承重墙标记,再顺着来路指示牌回到上一层。看懂栈帧的构成,你就知道为什么崩溃时打印出的调用栈(backtrace)能一层层回溯——靠的正是这一串保存的 RBP 链条。

Sframe=8+8+Slocals+Spad\displaystyle S_{\text{frame}} = 8 + 8 + S_{\text{locals}} + S_{\text{pad}}
这个公式告诉你:一个空函数也至少要消耗 16 字节的栈空间,而局部变量越多,补齐造成的浪费可能越明显。
  1. 返回地址call 指令自动压入的 8 字节,指向调用者中下一条指令的地址。它是会议的「来路指示牌」。
    88
  2. 保存的 RBPpush rbp 压入的 8 字节,记录调用者帧的基准地址,供函数返回时恢复现场。
    88
  3. 局部变量区由 sub rsp, N 分配,编译器把局部变量按声明顺序排布在这块区域中。
    SlocalsS_{\text{locals}}
  4. 对齐补齐为了保证总帧大小满足 16 字节对齐要求,编译器会在局部变量尾部填充空白。
    Spad=(16−Slocals mod 16) mod 16S_{\text{pad}} = (16 - S_{\text{locals}} \bmod 16) \bmod 16

入栈出栈:升降梯的楼层管理

地下办公楼的楼层不是一层层预先盖好的,而是开会时临时往下挖。push 指令就像升降梯载着一个人下行一层:地板(RSP)先下降 8 字节,然后把东西放进刚腾出来的那格。pop 则相反,先把那格的东西取走,地板再回升 8 字节。

单格搬运效率太低,所以 sub rsp, N 像是直接用挖掘机一次性往下挖出 N 字节的大厅,add rsp, N 则是回填。编译器分配栈帧时几乎总用 sub/add,只有保存寄存器这类单值才用 push/pop。

这种「后进先出」的楼层管理和电梯一模一样:后进电梯的人站在门口,出电梯时反而最先出去。理解 push/pop 的移动方向,是读懂任何反汇编代码的基础——看到 sub rsp, 0x20,你就知道这层楼为局部变量挖了 32 字节的大厅。

push: RSP←RSP−8pop: RSP←RSP+8\displaystyle \text{push: } \text{RSP} \leftarrow \text{RSP} - 8 \qquad \text{pop: } \text{RSP} \leftarrow \text{RSP} + 8
栈向下增长、后进先出:越晚压入的数据在越低的地址,却会越早被弹出。
  1. push 压栈先把 RSP 向下移动 8 字节,再把操作数写入新位置。因为栈向低地址生长,所以是减 8。
    RSP←RSP−8\text{RSP} \leftarrow \text{RSP} - 8
  2. pop 弹栈从当前 RSP 指向的位置读出数据,然后把 RSP 向上回移 8 字节,完成释放。
    RSP←RSP+8\text{RSP} \leftarrow \text{RSP} + 8
  3. sub 分配一次下移 N 字节,为整个局部变量区一次性腾出空间,N 通常是 16 的倍数。
    RSP←RSP−N\text{RSP} \leftarrow \text{RSP} - N
  4. add 释放函数返回前回移 N 字节,把栈顶恢复到进入函数时的位置。
    RSP←RSP+N\text{RSP} \leftarrow \text{RSP} + N

RSP对齐:消防地砖上的会议桌

物业有一项硬规定:会议桌的四角必须卡在 16 厘米消防地砖的网格线上。为什么?因为仓库的叉车(SSE 的 movaps 等指令)只能沿网格线行驶,一旦桌子歪出网格,叉车就会撞墙——在 CPU 里表现为对齐错误异常(#GP)。

SysV ABI 把这条规定写死了:执行 call 之前,RSP 必须 16 字节对齐。但 call 本身会压入 8 字节的返回地址,所以当被调函数的第一条指令执行时,RSP 恰好“歪”了 8 字节。因此函数入口处 RSP 对 16 取模必须等于 8,这是初学汇编时最容易踩的坑。

编译器在 sub rsp, N 里悄悄加上的那些“多余”字节,就是消防地砖的补齐(padding)。这也是为什么你在反汇编里经常看到 sub rsp, 0x38 这种不是 8 的倍数的原因——它要同时满足 16 对齐和局部变量需求。

RSPbefore call mod 16=0⇒RSPentry mod 16=8\displaystyle \text{RSP}_{\text{before call}} \bmod 16 = 0 \quad \Rightarrow \quad \text{RSP}_{\text{entry}} \bmod 16 = 8
调用者负责在 call 前摆正桌子,被调函数则要在 prologue 里把自己重新摆正。
  1. call 前的对齐承诺ABI 要求每个函数在发起调用前,确保自身 RSP 是 16 的整数倍,这是调用者的责任。
    RSPbefore call mod 16=0\text{RSP}_{\text{before call}} \bmod 16 = 0
  2. call 压栈破坏对齐call 压入 8 字节返回地址,地板下降 8,于是函数入口处余数变成 8。
    RSPentry≡8(mod16)\text{RSP}_{\text{entry}} \equiv 8 \pmod{16}
  3. prologue 恢复对齐入口 push rbp 再降 8 字节,余数变回 0;之后的 sub rsp, N 只要 N 是 16 的倍数,就始终保持对齐。
    RSPprologue≡0(mod16)\text{RSP}_{\text{prologue}} \equiv 0 \pmod{16}

红区定义:地板下的免费暗格

每层楼开会时,地板正下方还有 128 厘米深的「暗格」——它的学名叫红区(Red Zone)。物业条例承诺:大扫除(信号处理)时绝不碰这个暗格;员工也不用打报告就能往里塞临时物品,完全免费。这块区域的地址范围是 RSP 正下方 128 字节。

但注意:暗格的好处只在「不叫外人进你楼层」时成立。一旦你调用其他函数,新来者的地板就铺在暗格顶上,你塞进去的东西会被直接盖住。所以 ABI 规定:红区只能存放不跨函数调用的临时数据。

把这个暗格拉进日常开发的是这样一条规则:叶函数(不再调用别人的函数)可以把整个局部变量区都塞进红区,连 sub rsp, N 都省了。GCC 在 -O2 下对小型叶函数几乎总是这么干,这也是红区最重要的工程价值。

RedZone=[RSP−128,  RSP)\displaystyle \text{RedZone} = [\text{RSP} - 128,\; \text{RSP})
左闭右开区间:从 RSP 下方 128 字节处开始,一直到 RSP 本身为止,一共 128 字节的免费储物区。
  1. 下边界红区起点是 RSP 减去 128 字节处的地址,这是暗格的最深处。
    RSP−128\text{RSP} - 128
  2. 上边界红区终点就是当前 RSP 位置,栈指针以下是暗格,以上是已用的栈帧。
    RSP\text{RSP}
  3. 区间闭开性左闭右开意味着 RSP 指向的地址本身不属于红区,暗格是 RSP 之下的一片连续空间。
    [RSP−128,  RSP)[\text{RSP} - 128,\; \text{RSP})

叶函数优化:独居办公室的自由

公司里有一种「独居办公室」——里面的人开完这场会就下班,绝不再叫任何人进自己楼层。这种函数叫叶函数(leaf function)。因为不会有人进来,他可以尽情使用脚下那块 128 厘米的暗格:把草稿纸、便签、临时记号全摊在暗格里,连正儿八经的会议桌都不用摆了。

翻译成汇编就是:叶函数无需 sub rsp 调整栈指针,直接用 RSP 加上负偏移访问红区里的局部变量。比如 mov [rsp-8], rax 就把 8 字节数据存进了红区。如果整个帧都能塞进 128 字节,prologue 和 epilogue 都可以大幅简化。

性能收益是实打实的:少了两条栈指针调整指令,还减少了栈访问的地址计算。所以你在反汇编里看到一个小函数既没有 push rbp,也没有 sub rsp,直接用 [rsp-8]、[rsp-16] 存取局部变量,那它多半就是住进红区的幸运儿。

Sframe≤128  ⇒  无需调整 RSP,帧直接使用红区\displaystyle S_{\text{frame}} \leq 128 \;\Rightarrow\; \text{无需调整 RSP,帧直接使用红区}
判断标准很简单:叶函数 + 栈帧总大小不超过 128 字节,编译器就让它住进红区。
  1. 叶函数条件函数体内没有任何 call 指令,即不会调用其他函数。这是使用红区的先决条件。
    leaf(f)⇔f 不包含 call 指令\text{leaf}(f) \Leftrightarrow f \text{ 不包含 call 指令}
  2. 帧大小限制整个栈帧(包括返回地址、保存的寄存器、局部变量)必须能塞进 128 字节的红区。
    Sframe≤128S_{\text{frame}} \leq 128
  3. 免调整收益帧放红区后,函数不再需要 sub rsp / add rsp 两条指令,RSP 全程不动。
    ΔRSP=0\Delta \text{RSP} = 0

信号安全:物业大扫除的承诺

你可能会担心:暗格虽好,物业大扫除时会不会把东西扫走?SysV ABI 给出了承诺:信号处理器不得修改红区。内核在递送信号时,会把信号帧(sigframe)垫在红区之下——先让 RSP 往下让出 128 字节,再往下构造信号帧,这样信号处理器来回跑一趟,你的暗格原封不动。

但注意,暗格的安全承诺只针对物业大扫除。真正让红区数据失效的是「别人进你楼层开会」——一旦当前函数调用其他函数,被调函数的栈帧就会占据这块位置。所以红区数据的有效期被严格限制在一次函数执行、且不跨任何调用的范围内。

RSPsigframe=RSPuser−128−Ssigframe\displaystyle \text{RSP}_{\text{sigframe}} = \text{RSP}_{\text{user}} - 128 - S_{\text{sigframe}}
内核通过「先让 128,再建帧」的方式,保护了用户函数红区里的临时数据。
  1. 让出红区内核先将用户 RSP 下移 128 字节,相当于替用户函数把暗格上方贴上“请勿踩踏”封条。
    RSPuser−128\text{RSP}_{\text{user}} - 128
  2. 构造信号帧在红区之下再向下分配信号帧大小的一块空间,用于保存上下文等信息。
    −Ssigframe- S_{\text{sigframe}}
  3. 返回恢复信号处理结束后 RSP 恢复原值,用户函数对红区数据的使用不受影响。
    RSP←RSPuser\text{RSP} \leftarrow \text{RSP}_{\text{user}}

调试验证:监控探头下的楼层检查

当程序崩溃或行为诡异时,调试器就是物业的监控探头。bt 命令能顺着保存的 RBP 链一层层回溯调用历史;info frame 会直接告诉你当前帧的 RSP、RBP 和帧大小;objdump -d 则像施工图复核,让你核对编译器生成的 prologue 是否符合对齐规则。

手工验证对齐有个简单口诀:打断在看函数入口处,RSP 对 16 取模必须等于 8。你可以用 GDB 的 p/x $rsp 直接查看,也可以静态算:call 前是 16 的倍数,压入 8 字节返回地址后必然余 8。

常见陷阱包括:在调用其他函数后仍访问红区数据、局部变量超过 128 字节却硬塞红区、忘了 call 那 8 字节导致对齐计算差 8。用 GDB 在入口和 sub 之后各断一次,对比 RSP 余数从 8 变 0,基本就能定位大多数栈帧问题。

RSPentry&0xF=8\displaystyle \text{RSP}_{\text{entry}} \mathbin{\&} 0\text{xF} = 8
位运算 & 0xF 等价于对 16 取模,结果 8 表示函数入口处“歪了 8 字节”,这是正常状态。
  1. 十六进制掩码0xF 的二进制是 1111,按位与后只保留最低 4 位,即 RSP 对 16 取模。
    RSPentry&0xF\text{RSP}_{\text{entry}} \mathbin{\&} 0\text{xF}
  2. 入口余数由于 call 压入了 8 字节返回地址,入口处 RSP 最低 4 位必须是 8。
    =8= 8
  3. prologue 后复检执行 push rbp 之后,最低 4 位应该变成 0,表示重新对齐。
    RSPprologue&0xF=0\text{RSP}_{\text{prologue}} \mathbin{\&} 0\text{xF} = 0

AI 应用场景

编译器后端开发:自动生成合规栈帧

阶段:代码生成与优化

解决的问题:手写汇编时,开发者常常忘记 16 字节对齐、把超过 128 字节的局部变量塞进红区,导致 SIMD 指令崩溃或数据被覆盖。编译器后端在生成 prologue 时,会像精算师一样自动计算 S_pad 补齐字节,并判断函数是否为叶函数、帧大小是否适合红区,从而选择「普通帧」或「红区帧」两种布局。

具体案例:Clang/LLVM 的 X86FrameLowering 负责生成栈帧调整指令。当你用 -O2 编译一个只做简单算术的纯函数,它生成的汇编往往没有 sub rsp,这正是后端判定该函数可住进红区后的优化结果。

内核信号处理:保护用户态红区

阶段:操作系统内核机制

解决的问题:信号是异步到达的,内核必须在不确定的时刻安全地插入信号处理帧。如果直接压栈,会覆写用户函数红区数据,破坏 ABI 承诺。内核通过「先让出 128 字节红区、再构造信号帧」的布局,保证信号处理前后用户栈帧完全还原。

具体案例:Linux 的 setup_rt_frame 在构造 sigframe 时先执行 rsp -= 128 再分配信号帧;GCC 也因此敢于放心地在红区存放叶函数临时变量,二者配合构成了 SysV ABI 的完整生态。

崩溃分析与栈回溯调试

阶段:故障诊断与可观测性

解决的问题:程序崩溃时,调试器需要沿着保存的 RBP 链回溯每个调用者的栈帧,确定崩溃位置。理解栈帧布局的人能直接根据 bt 输出判断是栈溢出、红区越界还是对齐错误,而不是盲目加日志。

具体案例:GDB 中 bt 显示的三级调用链,每一级对应一个 16 字节对齐的栈帧;info frame 显示 rsp=0x7ffd...0 表示某函数 prologue 已完成对齐。若看到 rsp 余数为 8 的异常帧,通常意味着发生了栈破坏。

Python 演示

python64 行
class StackSimulator:    """用 Python 模拟 x86_64 向下增长的函数调用栈与红区"""     def __init__(self, base=0x8000):        self.rsp = base          # 栈指针,向下增长        self.storage = {}        # 地址 -> 值     def push(self, value):        self.rsp -= 8        self.storage[self.rsp] = value     def pop(self):        val = self.storage[self.rsp]        del self.storage[self.rsp]        self.rsp += 8        return val     def enter_function(self, name, locals_bytes):        """模拟 call + push rbp + sub rsp,返回帧信息"""        self.push("ret_to_" + name)        self.push("old_rbp_of_" + name)        rbp = self.rsp        pad = (-locals_bytes) % 16        total = locals_bytes + pad        for i in range(0, total, 8):            self.push(f"local_{i}")        frame_size = 16 + total        return {"name": name, "rbp": rbp,                "frame_size": frame_size, "pad": pad}     def use_redzone(self, locals_bytes):        """叶函数:不调整 RSP,直接在红区存放局部变量"""        if locals_bytes > 128:            raise ValueError("红区只有 128 字节!")        for i in range(0, locals_bytes, 8):            self.rsp -= 8            self.storage[self.rsp] = "redzone_local_" + str(i)        return locals_bytes     def leave_ret(self, frame):        """用帧指针恢复 rsp,再 pop 旧 rbp 和返回地址"""        self.rsp = frame["rbp"]        self.pop()        return self.pop()  def main():    sim = StackSimulator()    frame_main = sim.enter_function("main", 24)    frame_func = sim.enter_function("func_leaf", 24)    rz_used = sim.use_redzone(16)     print("main  帧大小:", frame_main["frame_size"], "字节, 补齐:", frame_main["pad"])    print("func  帧大小:", frame_func["frame_size"], "字节, 补齐:", frame_func["pad"])    print("叶函数红区使用:", rz_used, "字节, RSP 未做 sub 调整")    print("当前 RSP 对齐:", sim.rsp % 16, "字节")     sim.leave_ret(frame_func)    sim.leave_ret(frame_main)    print("全部返回后 RSP:", hex(sim.rsp))  if __name__ == "__main__":    main()
目的:用 Python 逐字节模拟 x86_64 函数调用栈的建立、对齐补齐与叶函数红区使用。
  1. self.rsp -= 8push 的核心动作:栈向下增长,指针减 8 后写入新值
  2. pad = (-locals_bytes) % 16这是编译器计算 16 字节对齐补齐量的同一公式
  3. frame_size = 16 + total帧总开销 = 返回地址 8 字节 + 保存 RBP 8 字节 + 局部区
  4. if locals_bytes > 128:红区容量上限检查,超过则必须改用普通栈帧
  5. frame_func = sim.enter_function("func_leaf", 24)叶函数照样先建普通帧,说明「叶函数」不等于「无帧」
  6. rz_used = sim.use_redzone(16)叶函数把 16 字节局部变量直接放进红区,RSP 未做 sub 调整
  7. print("当前 RSP 对齐:", sim.rsp % 16, "字节")验证红区使用后 RSP 依然保持 16 字节对齐

常见误区

误区我一直以为 ABI 要求每个函数的第一条指令执行时,RSP 必须 16 字节对齐。所以我在函数开头直接用 `movaps [rsp], xmm0`,结果程序崩了,Debug 了很久。

正解ABI 的真实规定是:执行 call 之前 RSP 必须 16 字节对齐,而 call 本身会压入 8 字节返回地址,所以被调函数入口处 RSP 对 16 取模等于 8。函数在 prologue 里执行 push rbp(再降 8 字节)之后,RSP 才恢复为 16 对齐。把桌面理解成地砖就清楚了:进门前摆正桌子,推门(call)的动作把桌子带歪 8 厘米,进门后自己再摆正。

为何常见:教材和文档常把结论简写为「函数内 RSP 16 对齐」,省略了 call 压栈这 8 字节的中间态,导致很多人在入口处用对齐指令栽跟头。

误区ABI 说信号处理器不能修改红区,那我把重要的全局状态放在红区里应该很安全吧?反正物业承诺大扫除不碰它,这样访问还快。

正解红区的保护只针对信号处理器,不针对其他函数。一旦当前函数调用任何其他函数,被调函数的栈帧就会铺在红区位置,你的数据当场被覆盖。所以 ABI 明确规定红区只能存放「不跨函数调用」的临时数据。叶函数之所以能放心用红区,恰恰是因为它保证不调用别人。把红区当长期储物柜,等于把文件放在别人必经的地板上。

为何常见:大家记住了「信号不碰红区」这一半,忽略了「跨调用失效」另一半,于是把红区误当成永久的快速存储区。

误区既然叶函数不调用别人,那它肯定不需要栈帧,也一定不用 RBP 帧指针,我写汇编时直接省掉整个 prologue。

正解叶函数只是「不调用其他函数」,它可以有普通栈帧,也可能需要保存非易失寄存器、分配很大的局部数组。是否使用 RBP 取决于编译选项:-fomit-frame-pointer 开启时,非叶函数也可能不用 RBP;反之,开启调试信息时叶函数也可能用 RBP 方便回溯。红区优化只是一种可选策略,不是叶函数的强制形态。

为何常见:把「叶函数」和「无帧函数」两个概念混为一谈,又看到编译器对小型叶函数输出极简汇编,就以为所有叶函数都长这样。

误区红区只有 128 字节,那我函数里定义了一个 256 字节的数组,是不是编译器会报错或者直接栈溢出?

正解红区只是一层免费优化,编译器会先判断帧大小是否适合红区;超过 128 字节时,它自动改用在红区之上、调整 RSP 的普通栈帧布局。sub rsp, 0x108 这类指令就是为大方脸准备的。红区是「能省则省」的折扣,不是「非得用」的强制区。

为何常见:把红区的 128 字节误当成整个 x86_64 栈帧的容量上限,忽略了大栈帧随时可以靠调整 RSP 获得无限空间。

面试题

Q1. x86_64 System V ABI 中,红区位于 RSP 的哪个方向、大小是多少?

单选题 · easy

  • A.RSP 上方,大小 128 字节
  • B.RSP 下方,大小 128 字节← 正确答案
  • C.RSP 上方,大小 128 字(1024 字节)
  • D.RSP 下方,大小 128 字(1024 字节)

答案:B。红区是 RSP 正下方 128 字节的区域,地址范围是 [rsp-128, rsp)。它像地板下的暗格,向下延伸且免费使用。选 B。A 方向反了;C、D 把「字节」误写成「字」,x86_64 中字是 2 字节,128 字只有 256 字节且方向描述也混了。

Q2. x86_64 的栈增长方向是?

单选题 · easy

  • A.从高地址向低地址增长← 正确答案
  • B.从低地址向高地址增长
  • C.栈空间不会增长或收缩
  • D.由操作系统动态决定方向

答案:A。x86_64 栈向下增长,即每次压栈 RSP 递减,向低地址方向延伸。这就像地下办公楼越挖越深,房号越来越小。所以选 A。B 是堆或其他架构的增长方向;C、D 与硬件行为不符。

Q3. call 指令执行后,被调函数入口处 RSP 相对 16 字节对齐的状态是?

单选题 · mid

  • A.RSP 完全满足 16 字节对齐
  • B.RSP 对 16 取模后余数为 8← 正确答案
  • C.RSP 对 16 取模后余数为 4
  • D.对齐情况由被调函数决定

答案:B。ABI 要求 call 前 RSP 是 16 的倍数,而 call 压入 8 字节返回地址,所以进入被调函数时 RSP 余 8。就像进门推门把桌子带歪 8 厘米,进门后要自己摆正。选 B。A 是 prologue 之后的状态;C 在任何合法调用点都不出现;D 混淆了责任方。

Q4. push 指令执行后,RSP 的变化是?

单选题 · mid

  • A.RSP 增加 8 字节并写入新值
  • B.RSP 减少 8 字节并写入新值← 正确答案
  • C.RSP 减少 4 字节并写入新值
  • D.RSP 增加 4 字节并写入新值

答案:B。push 先把 RSP 向下移动 8 字节(因为栈向下增长),再把操作数写入新地址。相当于电梯先下行一层,再把货物放进新楼层。选 B。A 方向反了;C、D 把 8 字节误写成 4 字节,x86_64 压栈单位是 64 位即 8 字节。

Q5. 关于红区的正确描述有哪些?

多选题 · mid

  • A.大小固定为 128 字节区间← 正确答案
  • B.叶函数可免于调整 RSP 使用← 正确答案
  • C.信号处理器承诺不破坏红区← 正确答案
  • D.数据可跨函数调用长期保存

答案:ABC。红区是 RSP 下方 128 字节,ABI 规定信号处理器不修改它,叶函数可以把整个帧放进去免调 RSP,所以 A、B、C 正确。D 错误:一旦调用其他函数,新帧会覆盖红区,数据不能跨调用保存,这也正是红区只能放临时数据的原因。

Q6. 函数 prologue(开场序言)通常不包含以下哪项操作?

单选题 · mid

  • A.push rbp 保存旧帧指针
  • B.mov rbp, rsp 建立帧指针
  • C.sub rsp, N 分配局部空间
  • D.jmp 跳转到函数开头← 正确答案

答案:D。prologue 是函数入口处的准备序列,典型是 push rbp、mov rbp rsp、sub rsp N,分别对应贴承重墙标记、确立基准、摆会议桌。jmp 是跳转指令,属于控制流操作,不属于开场序言。选 D。

Q7. main 调用叶函数前,叶函数把 32 字节局部数据放入红区且未动 RSP。下列哪个行为会破坏该红区数据?

单选题 · hard

  • A.信号处理器异步到来
  • B.叶函数内再调用 printf← 正确答案
  • C.函数末尾执行 ret 指令
  • D.修改 RBP 寄存器的值

答案:B。红区被 ABI 保护,信号处理器不会破坏它,所以 A 不选;一旦叶函数调用 printf,printf 的栈帧会铺在红区上,数据必然被覆盖——所以「叶函数」的定义就是不再调用他人;ret 只是返回,不写红区;修改 RBP 不影响红区数据。选 B。

Q8. 以下哪些情况下编译器不应使用红区存放数据?

多选题 · hard

  • A.数据生命周期跨越了函数调用← 正确答案
  • B.叶函数中仅存活的临时变量
  • C.使用了 alloca 动态扩展栈空间← 正确答案
  • D.函数返回前就会被用尽的数据

答案:AC。跨函数调用的数据放红区会被新帧覆盖,所以 A 不能用红区;alloca 会动态调整 RSP,红区位置随之漂移且可能被后续调用挤占,所以 C 也不应依赖红区。B 和 D 都是红区的典型适用场景:叶函数内、用完即弃的临时数据。选 A、C。

Q9. SSE 指令 movaps(对齐移动)要求操作数地址满足什么条件?

单选题 · easy

  • A.操作数地址按 4 字节对齐
  • B.操作数地址按 8 字节对齐
  • C.操作数地址按 16 字节对齐← 正确答案
  • D.操作数地址按 64 字节对齐

答案:C。movaps 中的 a 代表 aligned,要求 16 字节对齐,否则触发 #GP 异常。这就是 SysV ABI 强制 RSP 16 字节对齐的直接原因——编译器无法保证栈上局部变量的 16 字节边界,除非栈帧本身卡在地砖线上。选 C。

Q10. 关于 x86_64 栈帧与 RBP 帧指针的说法,正确的有哪些?

多选题 · hard

  • A.保存的 RBP 链可以用于栈回溯← 正确答案
  • B.开启帧指针省略优化可不使用 RBP← 正确答案
  • C.红区数据可以存放在 RBP 之上的位置
  • D.16 字节对齐规则仅对 32 位代码生效率

答案:AB。调试器 bt 命令正是沿着各帧保存的 RBP 链条回溯调用历史的,A 正确;-fomit-frame-pointer 开启后,编译器让 RBP 回归通用寄存器身份,改用 DWARF 调试信息做回溯,B 正确。红区在 RSP 下方而非 RBP 上方,C 错误;16 字节对齐是 x86_64 SysV ABI 的规则,对 64 位代码生效,D 错误。选 A、B。