为何现代调用约定要通过寄存器传递可变参数?
背景观察
现代架构的调用约定(比如x86-64 SysV、AArch64的AAPCS64)明确规定:可变参数与普通固定参数的传递方式完全一致。比如x86-64平台的open(path, mode, cflags)调用中,path存入RDI寄存器,mode存入RSI,唯一的可变参数cflags存入RDX。
寄存器传递固定参数的优势很明显——能大幅减少栈操作开销。但当函数需要处理可变参数、调用va_start时,会发现一个反直觉的现象:va_start会把所有可能用于传参的寄存器(数量远多于实际传入的参数)都保存到栈中。
比如用vfprintf模拟实现printf时,x86-64平台的汇编开头是这样的(省略重复代码):
my_printf: endbr64 ; 几乎无条件保存寄存器参数到栈 subq $216, %rsp movq %rsi, 40(%rsp) <...> movq %r9, 72(%rsp) testb %al, %al je .L2 movaps %xmm0, 80(%rsp) <...> movaps %xmm7, 192(%rsp) ; 为调用vfprintf重新整理参数 .L2: movq %fs:40, %rax movq %rax, 24(%rsp) xorl %eax, %eax movl $8, (%rsp) movl $48, 4(%rsp) leaq 224(%rsp), %rax movq %rax, 8(%rsp) leaq 32(%rsp), %rax movq %rax, 16(%rsp) movq %rsp, %rcx movq %rdi, %rdx movl $1, %esi ; 最终调用vfprintf movq stdout(%rip), %rdi call __vfprintf_chk@PLT ... ; 省略结尾代码
这里的VA帧占用了192字节;类似地,AArch64版本会把x1到x7、q0到q7寄存器的内容压栈,占用184字节。
如果可变参数全程通过栈传递,实现会简单得多,开销也更低:va_start只需要把栈中可变参数列表的起始地址赋值给变量就行——这正是i386平台的实现方式(所有参数都走栈)。比如Linux/i386平台上相同的包装函数汇编:
my_printf: pushl %ebx subl $8, %esp call __x86.get_pc_thunk.bx addl $_GLOBAL_OFFSET_TABLE_, %ebx leal 20(%esp), %eax ; <--- 这就是va_start的实现 pushl %eax ; 把VA指针传给vfprintf pushl 20(%esp) pushl $1 movl stdout@GOT(%ebx), %eax pushl (%eax) call __vfprintf_chk@PLT
核心问题
既然栈传递可变参数更简单高效,为什么x86-64和AArch64平台要选择复杂且耗费资源的实现?(我曾推测可能存在“同一函数允许同时用固定参数和可变参数两种声明风格”的场景,但没找到实例,比如前面的open函数就不属于这种情况。)
解答
这种设计主要是为了兼容可变参数函数的调用场景、平衡固定参数性能,核心原因如下:
统一调用逻辑,避免调用方额外分支
如果可变参数单独走栈,调用方需要区分目标函数是固定参数还是可变参数——这会增加编译逻辑的复杂度,尤其是在函数指针调用这类编译期无法确定函数类型的场景。让可变参数和固定参数用相同传递规则,调用方无需额外判断,既简化了编译实现,也避免了分支带来的性能损耗。支持简单可变参数函数直接复用寄存器
对于只需要读取前几个可变参数的简单函数(比如自定义的可变参数工具函数),可以直接访问寄存器完成参数读取,无需触发完整的栈保存操作。这种场景下,寄存器传递的效率优势完全体现,比全程走栈更高效。契合C语言可变参数的语义一致性
C语言中,可变参数函数的声明(比如void foo(int, ...))和调用方式没有特殊区分,调用方传递参数的规则必须和固定参数一致。如果可变参数单独走栈,会违背这种语义一致性,导致语言层面的规则复杂化。最大化现代架构的寄存器性能优势
x86-64和AArch64都配备了更多通用寄存器,固定参数用寄存器传递能大幅降低栈操作开销——这是现代架构的核心性能优化点。为了不放弃这个优势,只能让可变参数也走寄存器,仅在函数内部需要解析可变参数时,才将寄存器内容同步到栈中供va_arg等宏使用。这种设计是性能优先的权衡:大部分可变参数函数的调用方无需解析参数(比如直接调用printf),此时寄存器传递的优势完全发挥;只有函数内部需要处理可变参数时才会产生额外开销,而这类场景的占比相对较低。
内容的提问来源于stack exchange,提问作者Netch

