Se olharmos para algumas convenções de chamada modernas, como o estilo SysV x86-64 ou o estilo AArch64 (documento aapcs64.pdf intitulado "Procedure Call Standard for the Arm® 64-bit Architecture"), vemos notas explícitas de que argumentos variádicos são passados da mesma forma que outros argumentos. Por exemplo, uma chamada de função open(path, mode, cflags)
em x86-64 obterá path em RDI, mode em RSI e (o único variádico) cflags em RDX.
Não há dúvida sobre passar conjunto de argumentos estáticos em registradores, é bom para economizar recursos. Mas se olharmos para uma função que então interpreta argumentos e então va_start
os chama, veremos que va_start
é convertido em colocar todos os argumentos possíveis (tipicamente, muito mais do que realmente presente) na pilha; por exemplo, a emulação completa de printf
via vfprintf
começa com (eu compactei linhas semelhantes para evitar listagens muito longas):
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
Aqui 192 bytes de quadro VA. Similarmente, a versão AArch64 empurra 184 bytes (x1..x7 e q0..q7).
Se a cauda variádica de qualquer chamada de função tivesse sido sempre colocada na pilha, as coisas teriam ficado muito mais simples no código e mais baratas no tempo de execução, porque todo o empacotamento e cópia não teriam sido necessários. va_start
teria sido reduzido a uma única movimentação da localização inicial da lista variádica (na pilha) para uma variável. É assim que realmente funcionava com o i386 (onde todos os argumentos eram passados na pilha). Saída de assembly do mesmo wrapper trivial para 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
Aqui, a questão : por que a implementação de argumentos variáveis, pelo menos para x86-64 e aarch64, é tão complicada e desperdiça recursos?
(Eu poderia imaginar que havia casos em que dois estilos, ambos com argumentos fixos e com uma lista variádica, deveriam ter sido igualmente permitidos em declarações de função da mesma função. Mas não conheço um caso para isso. O mencionado open
é improvável que seja o único.)
Parece resolvido : o fator que levou a esse design é a necessidade de dar suporte ao legado C de funções "sem protótipo" (detalhes na resposta de @fuz) da maneira como elas são declaradas, em lugares diferentes, com ou sem protótipo, mas o uso é consistente com a assinatura realmente aplicada.