想象一家繁忙的餐厅后厨:厨师长喊一声“做菜!”,帮厨们必须按固定顺序把食材放到指定的台面上——第一个位置放主菜原料,第二个位置放配菜,第三个位置放酱汁,第四个位置放香料。台面位置不能乱,否则厨师会拿错东西。x86_64 的 System V ABI(应用程序二进制接口)就像是后厨的“摆盘规则”:CPU 执行函数调用时,参数必须按固定顺序放入指定的寄存器,返回值也必须从固定的“出菜口”端出来。在这篇文章里,我们会认识这些台面位置——RDI、RSI、RDX、RCX 等寄存器,以及谁负责收拾台面(调用者保存)和谁保证现场不乱(被调用者保存)。
学会 x86_64 寄存器约定,你就能读懂反汇编中的函数调用、定位崩溃时的参数值、理解编译优化后的代码逻辑——这是底层调试与性能分析的必备“翻译手册”。
可视化演示
展示x86_64 System V ABI整数参数RDI/RSI/RDX/RCX/R8/R9传递、浮点XMM0-7传递与RAX返回值、调用者保存与被调用者保存寄存器分类,揭示调用约定核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
整数参数传递:RDI/RSI/RDX/RCX/R8/R9 前6参数:依次通过RDI/RSI/RDX/RCX/R8/R9寄存器传递 第7+参数:超出6个后通过栈传递从右向左压栈 返回值:RAX存返回值RDX存高64位 f(a1..a6) → RDI,RSI,RDX,RCX,R8,R9 前6个整数参数通过寄存器传递提升效率
参数传递规则
后厨的台面摆放有规有矩:主菜、配菜、酱汁、香料,各有各的固定位置。就算厨师长临时多要了一道小料,帮厨也知道先摆满台面上的六个指定位置,剩下的料才往临时备料架上放——这就是 System V ABI 的“台面规则”。
参数传递规则规定了函数调用时实参的“落点”。在 x86_64 的 System V ABI 中,前 6 个整数参数依次放入 RDI、RSI、RDX、RCX、R8、R9 这 6 个通用寄存器;浮点参数则单独排进 XMM0 到 XMM7 这 8 个向量寄存器。参数超过这些寄存器容量后,剩余参数按从右到左的顺序压入进程栈。
这套规则保证了编译器、操作系统的动态链接器、调试器之间有一份共同“菜单”:只要双方都遵循同一 ABI,调用者和被调用者无论由哪个编译器编译,都能准确对接。这也是 Linux 上 C/C++、Rust、Go 等语言能互相调用的底层基础。
理解了这套“台面规则”,看汇编时就能一眼认出每个参数去了哪里。接下来的章节,我们把 6 个整型槽位逐一拆开细看。
- 函数参数映射将函数
f的实参a_1到a_n按顺序分配给寄存器或栈。 - 整数入通用寄存器前 6 个整数参数各自占用一个通用寄存器,第
i个对应第i个通用寄存器。 - 浮点入向量寄存器浮点参数不计入整型计数器,而是按出现顺序进入 XMM 寄存器。
- 超出入栈多于寄存器容量的参数从右向左压入栈区,用
RSP偏移量访问。
整型参数寄存器
餐厅的传菜口前有 6 个固定凹槽,第一个凹槽永远放“主菜”,第二个放“配菜”,第三个放“酱汁”,第四个放“香料”,第五、第六个放追加的小料。厨师长喊“鱼香肉丝,米饭,辣酱,葱花”,帮厨一听就知道:鱼香肉丝进 RDI,米饭进 RSI,辣酱进 RDX,葱花进 RCX。
在 x86_64 System V ABI 中,RDI(Destination Index)、RSI(Source Index)、RDX(Data Register)、RCX(Counter Register)这 4 个寄存器被赋予了固定的传参职责。func(a, b, c, d) 调用时,编译器生成的汇编就是依次把 a 装入 %edi,b 装入 %esi,c 装入 %edx,d 装入 %ecx——注意这里用的是 32 位子寄存器(EDI/ESI/EDX/ECX),因为写 32 位寄存器会自动清零高 32 位,传参效率更高。
这 4 个寄存器是传统 x86 时代的通用寄存器在 64 位模式下的延续,它们的名字带着历史包袱:RDI 里的 “D“ 源于字符串操作的目标索引,RSI 的 ”S“ 源于源索引,RCX 的 ”C” 源自循环计数器。虽然现代编译器主要把它们当作普通传参槽位,但名字里的历史痕迹仍保留在指令集中。
当你用 gdb 在一个函数入口处 info registers 时,看到的 RDI/RSI/RDX/RCX 的值,正是调用方传进来的前四个实参——这是调试时最实用的“读心术”。记住这四个槽位,汇编阅读量瞬间减半。
- 第 1 个参数实参
a被装入 RDI,这是函数调用约定里的“第一顺位”。 - 第 2 个参数实参
b被装入 RSI,与 RDI 配对出现。 - 第 3 个参数实参
c被装入 RDX,对应第三个槽位。 - 第 4 个参数实参
d被装入 RCX,四个槽位全部就位。
浮点与附加寄存器
台面上除了 6 个整料凹槽,旁边还并排放着 8 个“湿料碗”——专门放酱汁、高汤这类要计量的液体配料。厨师长喊“两勺糖,三勺盐,多半碗高汤”,整数配料走凹槽,液体配料走湿料碗,互不挤占。
浮点参数走的 XMM0-XMM7 是 128 位宽的向量寄存器,每个能装下两个 double 或四个 float。System V ABI 规定:调用函数时,浮点参数按从左到右的顺序装入 XMM0、XMM1……,而整型参数独立编号。例如 func(int a, double b, int c, double d) 的分配是:a→RDI,c→RSI(因为 C 是第二个整型),b→XMM0,d→XMM1。整型与浮点各数各的,互不干扰。
R8 和 R9 是 x86_64 新增的通用寄存器,R8 承接第 5 个整数参数,R9 承接第 6 个。它们没有 16 位/8 位的子寄存器别名,只有 64/32 位形式(如 R8D 代表低 32 位),使用时要稍加留意。
这套“双轨制”是许多初学者的盲区——以为混排时 XMM 要等整型排完才用。实际两者同时编号、独立推进,理解这一点,读那些“一半整数一半浮点”的库函数调用时就不会被绕晕了。
- 整数与浮点分离编号
i_1是第一个整型参数,进入 RDI。 - 第一个浮点参数
f_1是第一个浮点参数,进入 XMM0。 - 第二个整数参数
i_2是第二个整型参数,进入 RSI,不与 XMM 槽位冲突。 - 第二个浮点参数
f_2进入 XMM1,整型与浮点的编号各自独立。
返回值约定
菜做好了,厨师从“出菜口”端出去——这个固定位置在 x86_64 ABI 里就是 RAX 寄存器。如果一道菜分量太大,一个盘子装不下(返回 128 位数值),那就 RAX 装低位,RDX 装高位,两个盘子拼一桌;如果是汤菜(浮点),就从 XMM0 这个“汤碗出口”端出。
几乎所有函数调用都以 RAX 作为主返回值寄存器。对于普通 int 或指针类型,返回值直接留在 EAX/RAX 中;对于 __int128 或 long long 构成的 128 位结构体,低 64 位放 RAX,高 64 位放 RDX;浮点数返回时放入 XMM0。若返回大型结构体(超过 16 字节),调用者会先在栈上预留一块内存,把内存地址作为隐藏的第一个参数传给被调用者,被调用者把结果写入该地址后,将该地址本身放入 RAX 返回。
调试时遇到“我的返回值哪去了”的问题,先看 RAX;如果函数签名里有 __int128 或大结构体,再补看 RDX 和调用前预留的栈地址。返回值约定的这套“出菜口”逻辑,让调用方与被调用方在结果交付上从不会扯皮。
- 主返回值落 RAX函数返回时,主结果写入 RAX。
- 扩展返回值落 RDX若返回类型超过 64 位,高 64 位写入 RDX。
- 拼接语义调用者把 RAX 与 RDX 组合,还原完整返回值。
调用者保存寄存器
后厨里有些台面是“用完即弃”的:切完葱花的案板,抹布一擦就能给下道菜用,不用特意保护。谁要用谁就自己留个心眼——如果你下道菜还要用这案板,请自己把葱花先收到碗里。这就是调用者保存寄存器的哲学。
System V ABI 规定 RAX、RCX、RDX、RSI、RDI、R8-R11 这 10 个通用寄存器为调用者保存(Caller-Saved),也叫易失性寄存器。含义是:函数内部可以随意修改这些寄存器,不需要事先保存原值;如果调用者希望在函数调用结束后仍保留这些寄存器中的值,调用者必须在调用之前自行压栈保存,调用结束后再从栈中恢复。
这套设计的精妙在于效率。函数体内大量临时计算都会用到 RAX(返回值)、RCX(移位计数)、RDX(乘除法扩展)这些高频寄存器,如果要求每个函数都保存恢复,指令开销会成倍增长。让“用完就扔”的寄存器免于保存义务,编译器就能自由地分配临时变量,生成更紧凑的代码。
编译器在优化时会在调用点自动插入 push/pop 指令来保护易失寄存器——这也是为什么反汇编代码里常见 push rbx 后跟一串 pop rbx 的“保护盾”模式。理解“谁用谁保护”这条责任链,你就掌握了阅读汇编 prologue/epilogue 的钥匙。
- 定义易失集合列出所有调用者保存的通用寄存器集合。
- 调用前备份调用者若需保留其中某个寄存器,调用前自行
push压栈。 - 调用后恢复调用结束、寄存器被被调用者改写后,调用者
pop恢复原值。
被调用者保存寄存器
后厨里另一些台面是“专属珍藏”:每位墩子师傅的祖传刀,用完必须擦净放回原处,别人碰坏了要赔。被调用者保存寄存器(Callee-Saved)就是这样——函数要借用这些“传家宝”寄存器,进门时得先把自己的东西挪开,出门前必须原样放回。
System V ABI 规定 RBX、RBP、R12-R15 这 6 个寄存器为被调用者保存(非易失性)寄存器。函数如果要在函数体内部使用这些寄存器,必须先 push 保存原值(通常放在栈帧 prologue 里),函数返回前再 pop 恢复。这样调用者就能放心地在函数调用前后持续持有这些寄存器的值,不必额外备份。
RBP 还兼作帧指针,许多函数用它来定位栈上的局部变量;编译器开启优化后(-O2)经常把 RBP 也当作普通寄存器使用,此时帧指针被优化掉(frame pointer omission),调试体验会有所下降,但性能更好。RBX 常被用来保存跨函数调用仍然需要的循环计数或数组基址——因为它是“不会丢”的。
判断一个寄存器是否被调用者保存,最简单的记忆法:从 R8 到 R11 是“自由身”,而 RBX、RBP、R12-R15 是“有主之物”。在调试循环崩溃时,如果发现 R12 的值在函数返回后发生了变化,几乎可以断定被调用者违反了 ABI 约定——这种 bug 往往藏得很深。
- 非易失集合列出被调用者保存的寄存器,函数内部不可随意丢弃其原值。
- 进入时保存函数 prologue 里将用到的非易失寄存器压栈。
- 返回前恢复函数 epilogue 里从栈中弹出原值,保证调用者视野中的值不变。
栈帧与栈指针
后厨再大的台面也有放不下的食材,这时就要往地上的备料柜里码——柜子从门口往最里面一层层叠放,新来的食材先放门口。进深越大的柜子,越贴墙存放的是最早放进去的东西。栈就是这样一种“后进先出”的备料柜。
栈指针 RSP(Stack Pointer)始终指向栈顶,也就是备料柜门口的位置。函数调用时,call 指令会把返回地址压栈,RSP 减 8;函数入口后,prologue 常把 RBP 压栈并把 RSP 赋给 RBP,建立当前函数的栈帧基址,随后通过 RSP 减去固定偏移为局部变量腾出空间。栈向低地址方向生长,所以“压栈”是 RSP 减,“弹栈”是 RSP 增。
System V ABI 还规定了 128 字节的“红区”(Red Zone):RSP 正下方 128 字节区域是函数私有领地,信号处理器不会踩踏它。叶子函数(不再调用其他函数的函数)可以放心在这片区域里暂存数据而不必调整 RSP,省掉两条指令的开销。
栈帧布局是理解调用约定的物理基础:返回地址压在栈顶,被保存的 RBP 位于其下,局部变量依次向低地址排列。用 gdb bt 查看调用栈时,帧与帧之间的链条正是靠 RBP 寄存器的链接串起来的。
- 对齐基数
call指令压入 8 字节返回地址,使进入函数时 RSP 满足 16 字节对齐要求。 - 调用者维护调用者在
call之前必须保证 RSP 是 16 的倍数,这通常意味着参数压栈后要补sub rsp, 8。 - 对齐的目的保证
movaps等需要 16 字节对齐的 SIMD 指令在栈上安全操作。
实战观察寄存器
理论学会了,真正到了后厨现场,你要学会“看菜识位”:一盘菜端出来,看它放在哪个出菜口,就知道它是主菜还是配菜;看帮厨往哪个凹槽放料,就知道这是第几个参数。反汇编阅读和调试器就是你的“后厨监控”。
以 gcc 编译一个简单 C 函数后,用 objdump -d 反汇编,你会看到 func 开头一串 mov %edi, ... 或直接使用 %edi 的指令——这正是参数已经躺在寄存器槽位里的证据。在 gdb 中,info registers rdi rsi rdx rcx 能直接打印前 4 个参数的值;遇到 printf 之类的库调用,断点在 call 指令前停下,看看寄存器就明白这次调用传了什么。
实际工作中最常用的观察技巧:gdb 里用 x/6gx $rsp 查看压栈的溢出参数,配合 disassemble 反汇编定位 call 指令前的 mov 模式,就能反推出函数的真实签名。对 AI 项目的 CUDA/C++ 混合代码调试、PyTorch 的 ATen 底层调用分析,这套寄存器阅读法尤其好用——当你在 JIT 编译出的汇编里看到 %rdi 承载一个张量指针时,你已经能断言它是函数的第一个参数了。
从“看汇编如看天书”到“看汇编如看菜谱”,距离只差对寄存器约定的熟练度。下一次排查段错误时,先 info registers 看一眼 RDI 指向的地址是否可读,往往比追着源代码猜节省一个小时。
- 指针参数第一个参数(如张量指针
p)存入 RDI。 - 长度参数第二个参数(如元素个数
n)存入 RSI。 - 步长参数第三个参数(如字节步长
s)存入 RDX。
AI 应用场景
场景1:AI框架底层调用链分析
阶段:模型部署与推理优化
解决的问题:PyTorch/TensorFlow 在 GPU 或 CPU 上运行时会生成或调用大量底层 C++/CUDA 函数。模型推理速度异常或崩溃时,传统做法是盲猜源码;而按 ABI 规则读取寄存器,可以直接在汇编层还原函数参数——崩溃时 RDI 指向的地址是否为有效张量指针、RSI 中的维度值是否合理、RDX 中的步长是否越界,几条命令就能定位问题根因。
具体案例:某推理服务在 batch size 从 1 调到 32 后偶发 SIGSEGV。工程师用 gdb 挂住崩溃现场,info registers 发现 RDI(第一个参数)指向的地址落在未映射区域,顺着这个指针反查发现是上游把张量释放后未置空。整个过程未读一行框架源码。
场景2:JIT 编译代码调试
阶段:模型编译(JIT/AOT)
解决的问题:TVM、TorchScript、XLA 等编译器会动态生成机器码。生成代码若违反 ABI 约定,会在函数调用边界处产生“寄存器污染”——被调用者保存寄存器被改写,调用者却还以为值没变。理解 R12-R15 的保存义务,能快速区分“编译器 bug”和“手写汇编 bug”。
具体案例:某团队手写了一个 SIMD 优化算子,集成进 TVM 后随机产生错误结果。反汇编发现算子在函数尾部调用了一个辅助函数,辅助函数内部改写了 RBX 未恢复,导致主调用方的循环计数错乱。修正为 push rbx / pop rbx 后问题消失。
场景3:GPU Kernel 参数映射分析
阶段:CUDA 内核开发
解决的问题:CUDA kernel 启动时参数通过常量内存传递,但 CPU 侧调用约定仍是 System V ABI。理解 RDI/RSI 的映射规则,有助于在 cuda-gdb 中验证 host 侧传给 kernel 的参数正确性,尤其当 kernel 接受结构体参数、参数布局容易错位时。
具体案例:调试一个自定义 attention kernel 时发现 blockDim 计算异常。在 launch 处查看寄存器,发现第二个参数(grid dim)被错误地存入了 RDX 的高 32 位,导致后续 %edx 读取到垃圾值。修正参数构造代码后 kernel 行为恢复正常。
Python 演示
# -*- coding: utf-8 -*-"""模拟 x86_64 System V ABI 的参数寄存器分配规则"""INT_REGS = ["RDI", "RSI", "RDX", "RCX", "R8", "R9"]FP_REGS = ["XMM0", "XMM1", "XMM2", "XMM3", "XMM4", "XMM5", "XMM6", "XMM7"]CALLEE_SAVED = ["RBX", "RBP", "R12", "R13", "R14", "R15"]CALLER_SAVED = ["RAX", "RCX", "RDX", "RSI", "RDI", "R8", "R9", "R10", "R11"] def assign_args(args): """args 是 (名称, 类型) 列表,类型为 'int' 或 'float'""" int_idx = 0 fp_idx = 0 mapping = {} stack_overflow = [] for name, arg_type in args: if arg_type == "float": if fp_idx < len(FP_REGS): mapping[FP_REGS[fp_idx]] = name fp_idx += 1 else: stack_overflow.append((name, "float")) else: if int_idx < len(INT_REGS): mapping[INT_REGS[int_idx]] = name int_idx += 1 else: stack_overflow.append((name, "int")) return mapping, stack_overflow def split_return_128(value): """将 128 位整数拆成 RAX(低64位) 和 RDX(高64位)""" low = value & 0xFFFFFFFFFFFFFFFF high = (value >> 64) & 0xFFFFFFFFFFFFFFFF return low, high def main(): print("== x86_64 System V ABI 寄存器分配模拟 ==") func_signature = [ ("a", "int"), ("b", "float"), ("c", "int"), ("d", "float"), ("e", "int"), ("f", "int"), ("g", "int"), ("h", "float"), ] mapping, overflow = assign_args(func_signature) print("参数分配:") for reg, name in mapping.items(): print(" %-5s <- %s" % (reg, name)) if overflow: print("栈溢出参数:") for name, arg_type in overflow: print(" [栈] %s (%s)" % (name, arg_type)) print() print("函数返回 128 位值示例:") result = 0x1122334455667788AABBCCDDEEFF0011 low, high = split_return_128(result) print(" 完整值: 0x%032X" % result) print(" RAX(低64位): 0x%016X" % low) print(" RDX(高64位): 0x%016X" % high) print() print("寄存器分类:") print(" 调用者保存(易失):", ", ".join(CALLER_SAVED)) print(" 被调用者保存(非易失):", ", ".join(CALLEE_SAVED)) if __name__ == "__main__": main()
INT_REGS = ["RDI", "RSI", "RDX", "RCX", "R8", "R9"]定义前 6 个整型参数寄存器槽位,与 ABI 逐一对齐。mapping[FP_REGS[fp_idx]] = name浮点参数独立编号,按顺序写入 XMM0、XMM1…… 与整型互不干扰。stack_overflow.append((name, "int"))超出寄存器容量的参数记录到栈区列表,模拟从右向左压栈的溢出行为。split_return_128(value)演示 128 位返回值按“低 64 位归 RAX、高 64 位归 RDX”的拆分规则。CALLER_SAVED` 与 `CALLEE_SAVED展示调用者保存与被调用者保存两大类寄存器的成员划分,方便记忆对照。
常见误区
误区我以为是按参数位置统一数数,函数 `func(1, 2.5, 3)` 里的 2.5 应该排在第二个槽位,也就是 RSI,结果 2.5 却进了 XMM0,搞得我调试时对不上号。
正解整型参数和浮点参数是两套独立的编号系统!整型按顺序进 RDI/RSI/RDX/RCX/R8/R9,浮点按顺序进 XMM0-XMM7。func(1, 2.5, 3) 中,整数 1 进 RDI,浮点 2.5 进 XMM0,整数 3 进 RSI——因为它是第二个整型参数。记住“整型排整型的队,浮点排浮点的队”,各自独立计数。
为何常见:我们习惯按“第几个参数”的线性思维理解传参,但 ABI 是按参数类型分组排队,不是按位置全局编号,这与人脑的自然直觉相冲突。
误区我以为每次函数返回后 RDX 都装着返回值的高 64 位,所以我在每次调用后都去读 RDX,发现值经常是乱七八糟的数,完全对不上我的结果。
正解RDX 只有在返回值超过 64 位时才携带高 64 位数据(如 __int128 或 16 字节结构体)。普通 64 位及以下的返回值只用 RAX,RDX 在这时是“自由身”,可能被函数内部当作临时寄存器随意改写。判断标准很简单:看函数签名。返回 int/指针只信 RAX;返回 __int128 才把 RAX、RDX 拼起来看。
为何常见:教材里讲 128 位返回时强调了 RDX,初学者容易把特例当通例,忽略 RAX 是唯一恒定返回值寄存器这一事实。
误区既然是“调用者保存”,那函数返回后这些寄存器肯定是脏的,每次调用完必须重新加载。为了防止出错,我干脆把所有寄存器都压栈保存一遍。
正解调用者保存寄存器的意思是“函数可以不用保护就随意改写”,但实际是否改写取决于函数体内容。一个简单的函数可能根本没碰 RCX,调用后 RCX 里的值依然完好。正确做法是“如果需要,就保存;如果不需要,就不管”。编译器优化时还会精确分析哪些寄存器真会被破坏,只保护必要的。无脑全保存会把性能拖垮。
为何常见:“会保存”与“必须保存”是两件不同的事——前者是义务,后者是事实。初学者容易把 ABI 的最大授权误解为必然行为。
误区RBP 属于被调用者保存寄存器,那我只要按规矩 push/pop 就可以随意存放临时数据,毕竟它就是个寄存器嘛。
正解RBP 身兼两职:既是非易失寄存器,又是调试器定位栈帧的“锚点”。没有 -fomit-frame-pointer 优化时,调试器依靠 RBP 链回溯调用栈;你把它当临时变量改写,虽然 push/pop 义务尽到了,但栈回溯链会断裂,gdb bt 看到的调用栈将是一团乱麻。在开启优化的发布版本里编译器会频繁征用 RBP,但在未优化代码中请珍惜它的帧指针身份。
为何常见:寄存器编号相同容易让人忽略 RBP 的语义角色,误以为它和 RBX 完全等价。它的“双重身份”是调试世界与执行世界的交汇点。
误区数组是往高地址长的,我想当然以为栈也是从低地址往高地址长,结果手写汇编时用 `add rsp, 8` 压栈,直接把函数搞崩了。
正解x86_64 的栈是向低地址方向生长的!push 让 RSP 减小 8,pop 让 RSP 增加 8。局部变量分配空间用的是 sub rsp, 48,释放用的是 add rsp, 48。红区也是 RSP 下方的 128 字节。方向反了,栈指针就会往函数调用方的栈帧里乱撞,数据被覆写得一塌糊涂。
为何常见:内存地址从低到高的直觉太强了,而栈恰好反其道而行之——这是计算机系统中少数几个“向下生长”的数据结构,违背了人的空间直觉。
面试题
Q1. 在 x86_64 System V ABI 中,函数的第 1 个整型参数放在哪个寄存器?
- A.RAX
- B.RDI← 正确答案
- C.RDX
- D.RSI
答案:B。第 1 个整型参数固定放入 RDI。RAX 是返回值寄存器,RDX 是第 3 个参数寄存器,RSI 是第 2 个参数寄存器。就像第一个凹槽永远放主菜,RDI 永远是第一个整型参数的“家”。
Q2. System V ABI 规定前几个整数参数可以使用寄存器传递?
- A.4 个
- B.5 个
- C.6 个← 正确答案
- D.8 个
答案:C。RDI、RSI、RDX、RCX、R8、R9 共 6 个通用寄存器传递前 6 个整数参数。超过 6 个的额外参数按从右向左顺序压入栈。这 6 个槽位就像台面的 6 个固定凹槽,满了才往备料架(栈)放。
Q3. 一个 __int128 类型的返回值,低 64 位和高 64 位分别存放在哪里?
- A.RAX 低 64 位,RDX 高 64 位← 正确答案
- B.RDX 低 64 位,RAX 高 64 位
- C.RAX 低 64 位,RCX 高 64 位
- D.XMM0 低 64 位,XMM1 高 64 位
答案:A。128 位返回值按 RAX 存放低 64 位、RDX 存放高 64 位的约定拆分,调用者把两者拼合恢复完整值。这就像双拼披萨,RAX 装左半边、RDX 装右半边,合在一起才是完整的一盘菜。
Q4. 以下哪些寄存器属于调用者保存(易失性)寄存器?
- A.RAX← 正确答案
- B.RBX
- C.RCX← 正确答案
- D.R8← 正确答案
答案:ACD。RAX、RCX、R8 都是调用者保存寄存器,函数内部可随意改写;RBX 属于被调用者保存寄存器,使用前必须 push/pop。调用者保存寄存器像用完即弃的案板,被调用者保存寄存器像用后复位的祖传刀。
Q5. 函数 func(int a, double b, int c) 的第三个整型参数 c 存放在哪个寄存器?
- A.RCX
- B.RDI← 正确答案
- C.RSI
- D.RDX
答案:B。整型参数独立计数,a 是第 1 个整型进 RDI,c 是第 2 个整型进 RSI;浮点参数 b 进 XMM0。整型与浮点各排各的队,不能按总参数位置数。这里 c 排在总参数第 3 位,但它只是第二个整型,所以进 RSI。
Q6. 浮点参数在 System V ABI 中使用哪些寄存器传递?
- A.RDI-RDI 系列通用寄存器
- B.XMM0 到 XMM7← 正确答案
- C.RAX 与 RDX
- D.R8 与 R9
答案:B。浮点参数按顺序放入 XMM0-XMM7 共 8 个向量寄存器,每个都是 128 位宽,可容纳两个 double 或四个 float。XMM 寄存器像料理台旁的湿料碗,与整型的干料凹槽分开陈列。
Q7. 以下哪些是 x86_64 System V ABI 中被调用者保存(非易失性)寄存器?
- A.R12← 正确答案
- B.R13← 正确答案
- C.RSI
- D.RBP← 正确答案
答案:ABD。R12、R13、RBP 都属于被调用者保存寄存器,函数使用前必须压栈保存、返回前恢复。RSI 是调用者保存寄存器。被调用者保存寄存器像“完璧归赵”的传家宝,借用者必须原样归还。
Q8. 超过 6 个整数参数的函数,第 7 个整数参数放在哪里?
- A.放入 R10 寄存器
- B.放入 XMM0 寄存器
- C.按从右向左顺序压入栈← 正确答案
- D.直接丢弃
答案:C。超过 6 个的整数参数按从右向左的顺序压入栈区,调用者通过 RSP 偏移访问。栈是唯一接纳溢出参数的区域,就像台面凹槽满了后食材被放到备料柜里,按后进先出的顺序叠放。
Q9. System V ABI 栈对齐规则要求,被调用函数入口处(call 指令已执行)RSP 满足什么条件?
- A.RSP 是 4 的倍数
- B.(RSP + 8) 是 16 的倍数
- C.RSP 是 16 的倍数← 正确答案
- D.RSP 是 8 的倍数
答案:C。调用前 RSP 必须是 16 的倍数,call 压入 8 字节返回地址后,进入函数时 (RSP+8) 是 16 的倍数。这个约束保证 16 字节对齐的 SIMD 指令(如 movaps)能安全操作栈内存。
Q10. 关于 x86_64 System V ABI 的红区(Red Zone),以下说法正确的是?
- A.位于 RSP 正下方的 128 字节区域← 正确答案
- B.信号处理器不会破坏这片区域← 正确答案
- C.叶子函数可在此暂存数据而不调整 RSP← 正确答案
- D.位于 RSP 正上方的 128 字节区域
答案:ABC。红区是 RSP 下方的 128 字节私有领地,信号处理器保证不踩踏它,因此叶子函数可以把临时数据放在红区里、不需要 sub rsp 腾空间。它是“零成本”的临时存储区,但只有不再调用其他函数的叶子函数才能放心使用。D 的方向说反了,栈向下生长,红区在 RSP 之下。