为什么现代调用约定在寄存器中传递可变参数?

问题描述 投票:0回答:1

如果我们查看一些现代调用约定,例如 x86-64 SysV 风格 或 AArch64 风格(标题为“Arm® 64 位架构的过程调用标准”的 aapcs64.pdf 文档),我们会看到明确的注释,即可变参数以与其他参数相同的方式传递。例如,x86-64 上的函数调用

open(path, mode, cflags)
将获取 RDI 中的路径、RSI 中的模式以及 RDX 中的(唯一的变量)cflags。

在寄存器中传递静态参数集是没有问题的,有利于节省资源。但是,如果我们研究一个解释参数并为它们调用

va_start
的函数,我们会看到
va_start
被转换为将所有可能的参数(通常比实际存在的参数多得多)放入堆栈;例如,通过
printf
完全模拟
vfprintf
开头(我压缩了类似的行以避免列表太长):

my_printf:
        endbr64
; nearly unconditional saving
        subq    $216, %rsp
        movq    %rsi, 40(%rsp)
<...>
        movq    %r9, 72(%rsp)
        testb   %al, %al
        je      .L2
        movaps  %xmm0, 80(%rsp)
<...>
        movaps  %xmm7, 192(%rsp)
; repacking into registers for enclosed 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
; finally, call the function
        movq    stdout(%rip), %rdi
        call    __vfprintf_chk@PLT
... skipped epilogue

这里是 192 字节的 VA 帧。同样,AArch64 版本推送 184 字节(x1..x7 和 q0..q7)。

如果任何函数调用的可变尾部始终放在堆栈上,那么代码会变得更简单,运行时也会更便宜,因为不需要所有打包和复制。

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 ; <--- This is va_start
        pushl   %eax ; VA pointer pushed for vfprintf
        pushl   20(%esp)
        pushl   $1
        movl    stdout@GOT(%ebx), %eax
        pushl   (%eax)
        call    __vfprintf_chk@PLT

这里,问题:为什么可变参数实现(至少对于 x86-64 和 aarch64 而言)如此复杂且浪费资源?

(我可以想象,在某些情况下,在同一函数的函数声明中应该同等允许两种样式,都具有固定参数和可变参数列表。但我不知道这种情况。提到的

open
不太可能是那个。)

c assembly x86-64 arm64 calling-convention
1个回答
0
投票

请注意,并非所有调用约定都这样做。 例如,macOS 上使用的 AArch64 调用约定在堆栈上传递可变参数。

也就是说,在寄存器中传递可变参数的一个关键动机是,这样调用者和被调用者都不需要知道函数是否是可变参数。 例如,如果您要调用声明如下的无原型函数:

int printf();

您将无法知道它是否是可变参数函数。 但是,由于可变参数和非可变参数函数具有相同的调用约定,调用者可以简单地将 AL 设置为可变参数函数并调用它,如果不是,则被调用者忽略 AL。

这在 macOS 调用约定中是不可能的,在这种调用约定中,执行与原型不一致的可变参数函数的程序将会失败。

© www.soinside.com 2019 - 2024. All rights reserved.