想象你走进一家名为「x86_64 大酒楼」的餐厅。你点了一桌菜(调用一个函数),后厨(被调函数)热火朝天地忙活,终于把菜炒好了。问题是:这一盘盘菜是怎么递到你手上的?答案藏在餐厅的「出餐窗口」里。这家酒楼有个铁规矩:所有炒菜(整数返回值)从主窗口 RAX 递出;如果是双拼大份(128 位返回值),第二份从副窗口 RDX 递;甜点(浮点返回值)走甜点窗口 XMM0;要是点了全家桶(结构体返回值),后厨还会按大小决定「打包盒怎么递」或「要不要你自带保温箱」。这套出餐规矩写在餐饮行业协会发布的《标准操作手册》里,业内管它叫 ABI(应用二进制接口)。它保证了无论哪家饭馆(哪种语言编译出来的程序),只要按手册出餐,客人(另一个程序)就能稳稳接住。今天,我们就当一回后厨学徒,把这份出餐手册从头到尾翻一遍。

岗位关联

理解返回值寄存器约定,是读懂汇编、排查崩溃现场、编写跨语言绑定与高性能库的底层基本功。AI 从业者掌握它,能在模型算子优化、FFI 集成和编译器后端工作中游刃有余。

目录
  1. 可视化演示
  2. 整数返回约定:主窗口 RAX
  3. 浮点返回通道:甜点窗口 XMM0
  4. 结构体返回路径:打包盒与双槽
  5. 大结构体返回:自带保温箱 RDI
  6. ABI 调用约定:行业手册 System V
  7. 编译优化策略:老后厨的出餐心得
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示整数返回值RAX/RDX、浮点返回值XMM0与结构体返回值寄存器/指针传递规则,交互理解x86_64 ABI返回值约定。

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

'+modeData.integer.caption+'

整数返回约定:主窗口 RAX

你走进「x86_64 大酒楼」,点了一份红烧肉。后厨一阵叮当响后,炒好的肉被端到了主窗口 RAX 上——不管这道菜是糖醋里脊还是清蒸鲈鱼(int、指针、枚举),只要它是「一道菜」(64 位以内的整数),餐厅就约定从 RAX 这个固定窗口递出。客人(调用者)根本不用猜,直接去主窗口取就行。这个「约定俗成」就是寄存器返回约定的雏形。

要把这套规矩变成整个行业的共识,就需要白纸黑字的《出餐手册》——System V ABI。手册明确写着:所有 64 位以内的整数、指针、枚举值,一律存放在 RAX 寄存器返回。如果函数要返回的是 __int128 这种「两个菜一锅出」的大份单品,那么额外开一个副窗口 RDX:RAX 放低 64 位,RDX 放高 64 位。两个窗口拼一起,128 位的菜就完整递出去了。

编译器(后厨总管)在生成代码时,会在函数结尾放 mov rax, 返回值 或 mov rdx, 高64位 这类指令,把结果规规矩矩地送到窗口上。调用者则从同一个窗口接住。返回 char 或 short 这类小尺寸菜时,厨房还会「顺手扩个盘」——把高位按符号或零填充好,避免客人拿错尺寸。

所以,主窗口 RAX 承载了后厨绝大多数快递业务。你下次读汇编时,只要看到函数结尾往 RAX 里塞东西,就知道「菜」已经端到窗口上了。如果同时还动了 RDX,说明这是一份 128 位的「大份双拼」,别忘了两个窗口一起看。

函数返回 v⇒{RAX=低 64 位(v)RDX=高 64 位(v)(v 为 128 位时)\displaystyle \text{函数返回 } v \Rightarrow \begin{cases} \text{RAX} = \text{低 64 位}(v) \\ \text{RDX} = \text{高 64 位}(v) \quad (v \text{ 为 128 位时}) \end{cases}
  1. RAX 低 64 位函数返回前,将整值 v 的低 64 位写入 RAX。这便是调用者取回结果的唯一主窗口。
    RAX←低 64 位(v)\text{RAX} \leftarrow \text{低 64 位}(v)
  2. RDX 高 64 位当 v 的位宽超过 64 位(如 __int128)时,高 64 位写入 RDX 寄存器。
    RDX←高 64 位(v)\text{RDX} \leftarrow \text{高 64 位}(v)
  3. 符号扩展返回 int8_t/int16_t 时,编译器将值符号扩展为 32 位或 64 位再写入 RAX。
    RAX←SExt64(v)\text{RAX} \leftarrow \text{SExt}_{64}(v)

浮点返回通道:甜点窗口 XMM0

大酒楼除了炒菜窗口,还有专门的甜点窗口 XMM0。浮点数(float、double)就像蓝莓芝士蛋糕——它跟红烧肉在物理形态上完全不同,如果强行从炒菜窗口递,轻则糊一手奶油。于是行业手册规定:浮点返回值走 XMM0 专用窗口。这个隔离让「甜点」和「正餐」各走各的通道,互不污染。

XMM0 是 SSE/AVX 向量寄存器家族的一员,宽度 128 位及以上。浮点返回值按精度占用它的一部分:float 占低 32 位,double 占低 64 位。如果函数的返回值是 __m128 这样的向量类型,则直接使用完整的 XMM0,甚至扩展到 XMM0:XMM1 组合。

为什么不用 RAX?因为 x86 的通用寄存器(RAX 等)从设计上就不适合直接做浮点运算——早期 x87 浮点栈(ST(0))是单独的体系,SSE 普及后浮点又搬进了 XMM 家族。ABI 这样拆分「出餐窗口」,等于把甜点和正餐物理隔离。混合返回场景中,比如返回 struct { double d; long l; } 这种 16 字节结构体,分类规则会把它拆成 XMM0(d)和 RAX(l)两个窗口,各取所需。

甜点窗口的存在,让后厨不必为一道甜品停下炒锅——浮点结果与整数结果互不抢道。这也是 SIMD 时代留给 ABI 的清晰印记:每个返回值的「形态」都有自己专属的通道。

double/float 返回 v⇒XMM0=浮点位模式(v)\displaystyle \text{double/float 返回 } v \Rightarrow \text{XMM0} = \text{浮点位模式}(v)
  1. XMM0 写入将 v 的二进制位模式放入 XMM0 寄存器的低 64 位(double)或低 32 位(float)。
    XMM0←BitPattern(v)\text{XMM0} \leftarrow \text{BitPattern}(v)
  2. 单精度 float只写入 XMM0 的低 32 位,高位内容不被 ABI 保证。
    XMM0[31:0]←v\text{XMM0}[31:0] \leftarrow v
  3. 向量扩展返回 __m256 等 256 位向量时使用 YMM0,512 位使用 ZMM0。
    YMM0/ZMM0←v\text{YMM0}/\text{ZMM0} \leftarrow v

结构体返回路径:打包盒与双槽

如果客人点的是「小份全家桶」——一个不超过 16 字节的结构体,比如 struct Point { int x, y; }——后厨不会叫客人拿大箱子,而是直接拿两个标准打包盒(八字节槽位)装好,从 RAX 和 RDX 两个窗口同时递出。整个过程不需要经过一块额外内存。

System V ABI 对结构体返回值有一套「分类」算法:把结构体看作一串 8 字节槽位,再根据槽内字段的类型把每个槽归类为 INTEGER、SSE、MEMORY 等。若所有槽位都不是 MEMORY 类且总大小不超过 16 字节,就通过寄存器返回:第一个槽放 RAX,第二个槽放 RDX(需要时)。若结构体里混有 double 字段,对应槽位会被分类为 SSE,改走 XMM0/XMM1。

这个分类不是简单按大小切两半。比如 struct { char c; double d; } 大小 16 字节:第一槽 c + 7字节填充 是 INTEGER,第二槽 d 是 SSE,于是返回时两个槽分开走 RAX 和 XMM0。编译器会生成两条指令把两个槽塞进对应寄存器,调用者再拼接回结构体。这个细节在把 C 代码移植到汇编或写 FFI 绑定时极易踩坑。

「小份全家桶」用标准打包盒刚好装下,两个窗口一递,客人拎着就走,中间连一块内存都不需要碰——这就是小结构体寄存器返回的价值。

Ssize≤16B⇒RAX=槽1,RDX/XMM0=槽2\displaystyle S_{\text{size}} \le 16\text{B} \Rightarrow \text{RAX} = \text{槽1},\quad \text{RDX}/\text{XMM0} = \text{槽2}
  1. 大小判定编译器计算结构体 sizeof(S),若超过 16 字节则直接进入大结构体路径。
    sizeof(S)≤16\text{sizeof}(S) \le 16
  2. 八字节槽分组把结构体内存按 8 字节一组切分,得到若干槽位。
    槽i=S[8i:8i+8)\text{槽}_i = S[8i : 8i+8)
  3. 类型分类每个槽按内容字段类型合并为 INTEGER 或 SSE 等类别。
    Class(槽i)∈{INTEGER,SSE,MEMORY}\text{Class}(\text{槽}_i) \in \{\text{INTEGER}, \text{SSE}, \text{MEMORY}\}

大结构体返回:自带保温箱 RDI

现在客人点了「超级全家桶」——一个 64 字节的大结构体。打包盒塞不进出餐窗,怎么办?后厨和客人的约定是:客人自己带一个保温箱(栈上缓冲区),进门前先把箱子从 RDI 这个特殊通道递给后厨;后厨炒好后直接往箱子里装,装完再把箱子递回来。全程没有一个菜需要走窗口,也没有多搬一次手。

这正是 System V ABI 处理大结构体返回的方式:调用者(客人)在栈上分配一块足够大的内存缓冲区,把缓冲区的地址作为「隐藏的第 0 个参数」放入 RDI 寄存器,交给被调函数。被调函数把整个结构体的内容直接写入这块内存,然后返回,RAX 同时带上缓冲区地址以方便调用者链式使用。这里面没有多余的一次内存拷贝——写入就是最终位置。

这个设计的工程意义重大:如果按朴素思路「先在函数内构造一个临时结构体,再拷贝到栈上另一处给调用者」,64 字节的菜就会被来回搬运两次,缓存和带宽都被白白浪费。RDI 隐式指针方案把目标地址直接传给后厨,后厨在目标位置原地装盒。C++ 编译器在此基础上进一步做 RVO/NRVO:连函数体内的临时对象都省略掉,直接在缓冲区构造返回值。

保温箱方案少走两道窗口,大份菜也毫发无损——它是 ABI 设计中的「零拷贝」智慧,也是 Rust/C++ 大型对象返回值性能表现优异的底层原因之一。

Ssize>16B⇒RDI=缓冲区地址,写入 S 于 [RDI,RDI+sizeof(S))\displaystyle S_{\text{size}} > 16\text{B} \Rightarrow \text{RDI} = \text{缓冲区地址},\quad \text{写入 } S \text{ 于 } [\text{RDI}, \text{RDI}+\text{sizeof}(S))
  1. 栈上分配调用者预留 sizeof(S) 字节的栈空间作为返回缓冲区。
    buf=alloca(sizeof(S))\text{buf} = \text{alloca}(\text{sizeof}(S))
  2. RDI 传址将 buf 的地址放入 RDI,作为隐藏的第 0 个参数传给被调函数。
    RDI←buf\text{RDI} \leftarrow \text{buf}
  3. 直接写入被调函数把结构体数据写入 [buf, buf+size)。
    S→[RDI,RDI+sizeof(S))S \to [\text{RDI}, \text{RDI}+\text{sizeof}(S))
  4. 返回指针函数返回时 RAX 携带缓冲区地址,方便调用者链式使用。
    RAX←buf\text{RAX} \leftarrow \text{buf}

ABI 调用约定:行业手册 System V

前面说的所有窗口规则,并不是某一家饭店自创的,而是「餐饮行业协会」发布的统一手册——《System V ABI》。只要后厨照手册做,全世界的客人都能看懂你的出餐口。这也是为什么 C 写的库能被 Rust、Go、Python 扩展等不同「国别」的代码调用。

System V ABI(简称 psABI)是 x86_64 Linux、BSD 等类 Unix 系统遵循的二进制接口标准。它规定了参数怎么传(前 6 个整数参数依次用 RDI、RSI、RDX、RCX、R8、R9),返回值怎么接(整数用 RAX/RDX,浮点用 XMM0/XMM1),以及调用者保存 vs 被调者保存的寄存器边界。函数只需要在自己的躯体里保证:进来时遵守手册,出去时也遵守手册,整个程序就可以像流水线一样协作。

寄存器分为调用者保存(caller-saved,如 RAX、RCX、RDX、RSI、RDI)和被调者保存(callee-saved,如 RBX、RBP、R12-R15)。「调用者保存」的意思是:如果你在乎这些寄存器里的值,就得自己先存起来,因为被调函数可能随意改写它们。「被调者保存」则保证你从函数回来时,这些寄存器和进去时一样。返回值正好落在调用者保存区——因为它本来就是函数「交接」给调用者的物品,不需要额外保护。注意 Windows x64 的 ABI 与 System V 不同,参数寄存器前四个是 RCX、RDX、R8、R9,返回值寄存器则相似。跨平台写 JIT 或手写汇编时,选错手册会导致灾难。

行业手册的价值在于「统一」。你写的库、我写的框架、他用汇编抠的底层组件,只要都按 System V 出餐,就能严丝合缝地拼在一起。这也是 ABI 被称为二进制世界「通用语言」的原因。

整数返回→RAX,RDX;浮点返回→XMM0,XMM1\displaystyle \text{整数返回} \to \text{RAX}, \text{RDX}; \qquad \text{浮点返回} \to \text{XMM0}, \text{XMM1}
  1. RAX/RDX 通用返回第一个整数或指针返回值存放在 RAX,第二个在 RDX。
    RAX=ret1,RDX=ret2\text{RAX} = \text{ret}_1,\quad \text{RDX} = \text{ret}_2
  2. XMM0/XMM1 浮点返回第一个浮点返回值存放在 XMM0,第二个在 XMM1。
    XMM0=fret1,XMM1=fret2\text{XMM0} = \text{fret}_1,\quad \text{XMM1} = \text{fret}_2
  3. 独立分类返回值寄存器的分配只由返回类型自身决定,与函数参数个数无关。
    Class(RET)→Reg\text{Class}(\text{RET}) \to \text{Reg}

编译优化策略:老后厨的出餐心得

老练的后厨不会傻等菜出锅再找盒子——他们会在你递保温箱时就顺手把锅端到箱子边。编译器的 RVO(返回值优化)正是这个道理。所谓 RVO,是指编译器看到 T f() { T tmp; ... return tmp; } 这种写法时,干脆不在函数内部创建 tmp 的独立栈空间,而是直接在 RDI 指向的返回缓冲区里构造 tmp。于是 return 语句不再触发拷贝,整个函数一次拷贝都没有。

这是「别名」思想的胜利:编译器让局部变量与返回缓冲区共享同一块内存,所有对 tmp 的操作都在最终目的地完成。结构体越大,收益越明显——省掉的不只是一次 memcpy,而是缓存污染和两次全量搬家。C++17 甚至把某些场景的 RVO 从「优化」升级为「标准强制行为」,正是这层语义带来的保证。

除了 RVO,后厨还有几手绝活。寄存器分配:较小的返回值通过 RAX/RDX/XMM0 传递,编译器会优先把计算结果直接放进这些寄存器,避免临时存内存。尾调用优化:如果函数 A 的返回值就是调用函数 B 的返回值,编译器可以把 A 的返回地址也用作 B 的,直接把 B 的返回值收编为 A 的返回值,省掉一层调用栈。内联展开:小函数直接嵌进调用者体内,返回寄存器从「传递」变成了「无感交付」。理解这些优化,对设计高性能库的返回类型以及手写汇编都有直接指导意义。

后厨的本事,在于把「多一道手的搬运」化简到零。下一回你在 C++ 里返回一个大对象,心里该有数:是 RDI 保温箱加上 RVO 的巧手,让那盆全家桶稳稳落在客人桌上。

T f(){T t;…return t;}  ⇒  在 [RDI] 中原地构造 t, copy=0\displaystyle T\ f() \{ T\ t; \dots \text{return } t; \} \;\Rightarrow\; \text{在 } [\text{RDI}] \text{ 中原地构造 } t,\ \text{copy} = 0
  1. 临时消除编译器不为局部变量 t 分配独立栈帧,直接与返回缓冲区地址合一。
    buf≡t 的地址\text{buf} \equiv t \text{ 的地址}
  2. 构造下沉t 的构造和赋值语句全部直接作用于 RDI 指向的缓冲区。
    construct(t)→[RDI]\text{construct}(t) \to [\text{RDI}]
  3. 拷贝消除return t; 不再调用拷贝构造函数或 memcpy。
    memcpy=0\text{memcpy} = 0

AI 应用场景

场景一:AI 推理引擎的张量算子优化

阶段:模型推理与性能优化

解决的问题:AI 推理引擎(如 PyTorch CPU 版、ONNX Runtime)每层张量计算都会调用大量 C++ 算子。若算子返回 Eigen 固定大小矩阵或形状描述符(8~16 字节小结构体),不了解 ABI 时可能会习惯性返回指针或引用。正确利用「小结构体走 RAX/RDX 寄存器返回」的规则,可以省去堆分配与解引用,显著降低每算子纳秒级开销。这就像服务员不绕路去储物柜取打包盒,直接在出餐窗口递给你。

具体案例:PyTorch 的 TensorIterator 在广播形状推导时返回一种 16 字节以内的形状描述类型,编译器按 ABI 自动把结果放入 RAX/RDX,省一次内存访问。手写算子时,让返回类型保持在 16 字节以内并借助 ABI 寄存器传递,热点路径可提速 5%~15%。

场景二:ctypes / FFI 跨语言绑定返回值处理

阶段:工具链与集成调试

解决的问题:Python AI 生态常用 ctypes 调用 C/C++ 库,若 C 函数返回一个 C struct,ctypes 需要在调用前正确声明 restype,否则默认按 int 解析返回的 RAX,导致 3D 坐标、颜色值等结构体数据错乱。理解「小结构体走 RAX/RDX、大结构体走 RDI 隐式指针」后,可以快速定位这类静默错误:不是 Python 写错了,而是出餐窗口认错了。

具体案例:调用 Vulkan/OpenGL 数学库时,返回 struct { float x, y, z; }(12 字节),ctypes 中把 restype 设置为对应 Structure 类型,内部才会按 XMM0/RAX 规则正确拼接。若错误地把大结构体当小结构体声明,则会读到垃圾数据甚至段错误。

场景三:AI 编译器后端的返回值代码生成

阶段:编译优化与代码生成

解决的问题:TVM/XLA 等 AI 编译框架把算子描述编译为 LLVM IR 或直接生成机器码时,必须遵守目标平台 ABI 的返回值约定,否则生成的函数无法被宿主程序正确调用。特别是自定义调度返回聚合类型时,编译器后端会自动插入 RDI 隐式指针和 RAX 返回地址。这相当于后厨在开张前,先把《出餐手册》背得滚瓜烂熟。

具体案例:TVM 的 CodeGenCPU 在生成返回多个值的调度时,会把结果打包成小结构体按寄存器返回,或在结构体超过 16 字节时自动生成栈缓冲加 RDI 传递。理解这个约定,才能在调试 IR 时看清返回值被搬到了哪里。

Python 演示

python67 行
"""x86_64 System V ABI 返回值寄存器分配模拟器"""  def classify_int(nbits):    """整数返回值: <=64 位走 RAX, <=128 位走 RAX:RDX"""    if nbits <= 64:        return ['RAX']    if nbits <= 128:        return ['RAX', 'RDX']    raise ValueError('x86_64 ABI 不支持超过 128 位的整数返回')  def classify_float(typename):    """浮点返回值: float/double 走 XMM0"""    if typename in ('float', 'double'):        return ['XMM0']    raise ValueError('不支持的浮点类型: ' + typename)  class StructType:    """模拟 C 结构体, 只需名称与大小"""     def __init__(self, name, size):        self.name = name        self.size = size     def __repr__(self):        return 'struct %s (%dB)' % (self.name, self.size)  def classify_struct(st):    """结构体返回: <=16B 拆成两个八字节槽走寄存器, >16B 走 RDI 隐式指针"""    if st.size <= 8:        return ['RAX']    if st.size <= 16:        return ['RAX', 'RDX']    return ['RDI(隐式指针)', 'RAX(缓冲区地址)']  def get_return_locations(return_type):    """根据 System V ABI 规则, 返回返回值应使用的寄存器列表"""    if isinstance(return_type, int):        return classify_int(return_type)    if isinstance(return_type, str):        return classify_float(return_type)    if isinstance(return_type, StructType):        return classify_struct(return_type)    raise TypeError('未知返回类型: %r' % (return_type,))  def main():    cases = [        32,        64,        128,        'double',        StructType('Point2D', 8),        StructType('Point3D', 12),        StructType('Matrix4x4', 64),    ]    for case in cases:        locs = get_return_locations(case)        print('返回 %-16s -> %s' % (case, ' + '.join(locs)))  if __name__ == '__main__':    main()
目的:模拟 x86_64 System V ABI 的返回值分类器,输入返回类型(整数位宽、浮点类型、结构体大小),输出该返回值应使用的寄存器组合,帮助快速建立 ABI 直觉。
  1. if nbits <= 64: return ['RAX']64 位及以内的整数只有一个返回值槽,走主窗口 RAX。
  2. if nbits <= 128: return ['RAX', 'RDX']128 位整数需要主窗口加副窗口,两半拼成完整返回值。
  3. if typename in ('float', 'double'): return ['XMM0']浮点值统一走甜点窗口 XMM0,不占炒菜窗口。
  4. if st.size <= 16: return ['RAX', 'RDX']小结构体按两个八字节打包盒装进 RAX/RDX,无需内存。
  5. return ['RDI(隐式指针)', 'RAX(缓冲区地址)']大结构体由调用者经 RDI 递入保温箱,函数原地装盒。

常见误区

误区结构体字段多、体积大,返回值肯定是在栈上构造一份再拷贝给调用者。64 字节的大对象来回搬两次是没办法的事。

正解即使结构体达到 16 字节(如两个 double),也完全可以通过 RAX/RDX 或 XMM0/XMM1 寄存器返回,没有任何栈拷贝。只有大于 16 字节且分类为 MEMORY 时才走 RDI 隐式指针路径;而且该路径直接在调用者的缓冲区中写入,不存在「先构造再拷贝」的多余动作。大酒楼的保温箱方案就是为省掉这次搬运而设计的。

为何常见:人们把编译器的朴素实现(临时对象加拷贝)当成 ABI 唯一的实现方式,忽略了寄存器传递和隐式指针优化这两条更聪明的出餐路线。

误区函数返回值嘛,理应都存在 RAX 里。浮点数无非是把 RAX 当整数用,结构体也是先塞进 RAX 再读出来。

正解浮点返回走 XMM0/XMM1(SSE 向量寄存器),与 RAX 完全独立;大结构体返回则通过 RDI 传入缓冲区地址,数据本体根本不经过寄存器。RAX 只负责整数、指针以及部分小结构体返回。这就像甜点有自己的窗口,全家桶走保温箱,不是每道菜都非得从主窗口递。

为何常见:教材默认讲整数返回,容易把 RAX 想象成「唯一返回值寄存器」,忽略了浮点与聚合返回的分流设计。

误区RVO 无非是编译器帮我省掉一个拷贝构造函数,性能差别很小,不值得关注。

正解对于大结构体,RVO 省掉的是与函数返回值缓冲区之间的完整内存拷贝,可能是几十到几百字节的搬运,以及由此产生的缓存 miss。C++17 后部分 RVO 已是语言标准行为而非可选优化,理解它才能正确理解对象生命周期。后厨把锅直接端到保温箱边,省下的不是「一次端盘」,而是整条配送链路。

为何常见:小对象上拷贝代价低,反衬不出 RVO 的价值;大对象默认构造可能又慢又被复用,实际收益被严重低估。

误区网上资料说 x86_64 返回值放 RAX,那我写 Windows 程序也这么写肯定没问题。毕竟都是 x86 嘛。

正解System V ABI 只用于 x86_64 的 Linux、BSD、macOS 等类 Unix 系统;Windows x64 使用 Microsoft x64 ABI。虽然整数返回值也是 RAX,但参数传递(RCX、RDX、R8、R9)和寄存器保存规则不同,结构体返回的分类逻辑也有差异。两本手册都对,但饭馆的连锁店开到了不同城市,规矩得按当地分店来。

为何常见:大家讨论 ABI 时默认环境是 Linux,「x86_64」字眼掩盖了 Windows 与 Unix 之间的关键差异。

面试题

Q1. 在 x86_64 System V ABI 下,函数返回一个 64 位整数,结果存放于哪个寄存器?

单选题 · easy

  • A.累加器寄存器 EAX
  • B.返回寄存器 RAX← 正确答案
  • C.基址寄存器 RBX
  • D.参数寄存器 RDI

答案:B。System V ABI 规定 64 位整数返回值存放在 RAX,而不是 32 位的 EAX(EAX 只是 RAX 的低 32 位)或 RBX/RDI。就像大酒楼主窗口 RAX 才是递菜的固定位置,其他窗口各有用途,不能混用。

Q2. 函数返回 __int128 时,高 64 位存放在哪个寄存器?

单选题 · easy

  • A.低段部分 RAX
  • B.参数通道 RCX
  • C.高位副窗 RDX← 正确答案
  • D.栈顶指针 RSP

答案:C。x86_64 System V ABI 把 128 位返回值拆成两半:低 64 位放 RAX,高 64 位放 RDX。对应比喻:大份双拼菜分别走主窗口 RAX 和副窗口 RDX,两个窗口拼起来才是完整的一道菜。

Q3. 浮点 double 返回值存储在哪个寄存器?

单选题 · easy

  • A.FPU 浮点栈顶 ST(0)
  • B.向量寄存器 XMM0← 正确答案
  • C.通用寄存器 RAX
  • D.栈内存临时区

答案:B。System V ABI 规定 double/float 返回值经 SSE 寄存器 XMM0 传递,不再使用 x87 的 ST(0)。对应比喻:甜点走甜点窗口 XMM0,不占炒菜窗口 RAX;FPU 浮点栈是 32 位时代的老黄历。

Q4. 按 System V ABI,结构体返回值在多字节以内时通过寄存器返回?

单选题 · mid

  • A.不超过 8 字节
  • B.不超过 16 字节← 正确答案
  • C.不超过 32 字节
  • D.不超过 64 字节

答案:B。ABI 的分类规则中,结构体总大小不超过 16 字节,且每个 8 字节槽分类结果都不是 MEMORY 时,通过寄存器返回。超过 16 字节或任一槽为 MEMORY 则走 RDI 隐式指针。小份全家桶用 RAX/RDX 两个打包盒刚好装下。

Q5. 大结构体(如 64 字节的矩阵)作为返回值,通过什么机制传递给调用者?

单选题 · mid

  • A.存入静态全局变量区
  • B.RDI 隐式指针写入缓冲区← 正确答案
  • C.XMM0 配合栈区临时存储
  • D.RAX 多批次分块传递

答案:B。调用者先在栈上分配缓冲区,把地址放在 RDI 作为隐藏的第 0 个参数传给函数,函数直接把结果写入缓冲区,从源头避免栈拷贝。对应比喻:客人自带保温箱从 RDI 通道递进去,后厨原地装盒,一个菜都不走窗口。

Q6. 以下关于 System V ABI 返回值约定的说法,正确的有哪些?

多选题 · mid

  • A.整数返回值存放在 RAX← 正确答案
  • B.浮点返回值存放在 XMM0← 正确答案
  • C.指针返回值存放在 RDI
  • D.128 位整数高位在 RDX← 正确答案

答案:ABD。整数与指针都属于 INTEGER 类,通过 RAX 返回,所以 C 错,RDI 是参数寄存器;浮点走 XMM0;128 位整数由 RAX(低)+ RDX(高)拼接。三句话对应三个出餐窗口的职责,互不串位。

Q7. 关于 C++ 的 RVO(返回值优化),下列说法正确的是?

单选题 · hard

  • A.仅在 C++17 之后生效
  • B.消除大对象返回时的额外拷贝← 正确答案
  • C.仅对整数值类型返回有效
  • D.强制增加临时对象的构造

答案:B。RVO 让局部对象直接在 RDI 指向的返回缓冲区中构造,省掉 return 时的拷贝。C++17 只是把部分场景写成标准强制,但 GCC/MSVC 早在 C++11 前就实现了 RVO,因此 A 错;它主要针对类对象而非整数,C 错;它减少而非增加临时对象,D 错。

Q8. 在 x86_64 System V ABI 下,以下哪些说法正确?

多选题 · mid

  • A.返回 char 类型时值扩展到 32 位← 正确答案
  • B.返回 float 只用 XMM0 的低 32 位← 正确答案
  • C.返回指针使用 RDI 寄存器
  • D.返回 __int128 需要 RAX + RDX← 正确答案

答案:ABD。char 等窄整数返回会经 EAX 做符号或零扩展;float 只占用 XMM0 的低 32 位;__int128 拆成 RAX(低)和 RDX(高)。指针和整数一样走 RAX,所以 C 错。

Q9. 结构体 struct { int a, b, c; }(12 字节)作为返回值,寄存器分配的正确结果是?

单选题 · hard

  • A.三个字段全部存入 RAX 寄存器
  • B.前两个在 RAX 第三个在 RDX← 正确答案
  • C.因超过阈值走 RDI 隐式指针
  • D.仅首字段在 RAX 其余在 RDX

答案:B。12 字节不超过 16 字节,按 8 字节槽切分:低槽含 a、b(INTEGER),高槽含 c 与 4 字节填充(INTEGER),两个槽分别放入 RAX 与 RDX。不是所有字段都塞进 RAX,也不是走隐式指针。

Q10. 关于 ABI(应用二进制接口)的作用,以下说法正确的有哪些?

多选题 · hard

  • A.规定函数调用时参数的传递寄存器← 正确答案
  • B.规定返回值存放位置和字节宽度← 正确答案
  • C.决定 CPU 的指令集架构细节
  • D.保证不同语言编译产物可互操作← 正确答案

答案:ABD。ABI 是一套二进制层面的「出餐手册」,定义参数或返回值寄存器、栈布局、字节序等,让 C、Rust、汇编等不同编译器产物在同一平台协同工作(D)。它依赖但不决定 CPU 的微架构细节(C 错)。