制作可变参数函数被调用者清理

Make a variable argument function callee cleanup

假设我有一个函数:

int sumN(int n, ...)
{
    int sum = 0;
    va_list vl;
    va_start(vl, n);
    for (int i = 0; i < n; i++)
        sum += va_arg(vl, int);

    va_end(vl);
    return sum;
}

Called as sumN(3, 10, 20, 30); 函数为cdecl,意思是调用者清理。所以,发生的事情是这样的:

; Push arguments right-to-left
push 30
push 20
push 10
push 3
call sumN
add esp, 16 ; Remove arguments from stack (equivalent to 4 pops)

对于采用固定数量参数的常规函数​​,被调用方可以执行清理,作为 ret 指令的一部分(例如 ret 16)。这在这里不起作用,因为被调用者不知道有多少参数被推送 - 我可以将其称为 sumN(1, 10, 20, 30, 40, 50); 并导致堆栈损坏。

现在,无论如何我都想做。也许我有一个工具可以在构建之前解析源代码并确保所有调用都是合法的。我在我的代码库中调用了 sumN() 50k 次,所以最后一条指令的额外大小加起来了。

对于上面的实现,它很容易在汇编中完成,但如果它是一个 printf 函数或者计算大小的逻辑有点复杂的东西,那不再是一个选项。尽管如此,我还是可以进行一些内联​​汇编或其他操作,并修复 sumN 的实现以弹出堆栈。但是,如果有人有更好的解决方案,那是非常欢迎的。

然而,最大的问题是当它的声明中有 ... 时,如何告诉编译器该函数是被调用者清理?如何防止编译器生成add esp, 16指令?

理想情况下,我需要它用于 msvc、gcc 和 clang,但 msvc 是优先级。

相关:Can stdcall have a variable arguments?

你可以做的是制作一些辅助函数。每个辅助函数都将采用固定数量的元素,并且选择要调用的辅助函数将在编译时完成。然后,每个辅助函数都会调用您的可变参数函数。

每次调用您将节省一条指令,代价是 n 个辅助函数,其中 n 是可能参数的最大数量。

示例代码:

#include <stdio.h>
#include <stdarg.h>
#include <stdint.h>

#define GET_MACRO(_1,_2,_3,NAME,...) NAME
#define func(...) GET_MACRO(__VA_ARGS__, helper3, helper2, helper1)(__VA_ARGS__)

void varargFn(int n, ...)
{
        int sum = 0;
        va_list vl;
        va_start(vl, n);
        for (int i = 0; i < n; i++)
                sum += va_arg(vl, int64_t);

        va_end(vl);
        printf("%d\n", sum);
}

void helper1(void *v1)
{
        varargFn(1, v1);
}

void helper2(void *v1, void *v2)
{
        varargFn(2, v1, v2);
}

void helper3(void *v1, void *v2, void *v3)
{
        varargFn(3, v1, v2, v3);
}

int main()
{
        func((void *) 5);
        func((void *) 5, (void *) 5);
        func((void *) 5, (void *) 5, (void *) 5);

        return 0;
}

以及从 运行 gcc -s -Os -std=c99

生成的一小段
helper3:
.LFB14:
        .cfi_startproc
        movq    %rdx, %rcx
        xorl    %eax, %eax
        movq    %rsi, %rdx
        movq    %rdi, %rsi
        movl    , %edi
        jmp     varargFn
        .cfi_endproc
.LFE14:
        .size   helper3, .-helper3
        .section        .text.startup,"ax",@progbits
        .globl  main
        .type   main, @function
main:
.LFB15:
        .cfi_startproc
        pushq   %rax
        .cfi_def_cfa_offset 16
        movl    , %edi
        call    helper1
        movl    , %esi
        movl    , %edi
        call    helper2
        movl    , %edx
        movl    , %esi
        movl    , %edi
        call    helper3
        xorl    %eax, %eax
        popq    %rdx
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE15:
        .size   main, .-main

如果您设法避免跨寄存器的 n 元素的这种令人讨厌的移动,您可能可以从辅助函数中挤出更多字节。想到的一个想法是将 helper3 重写为:

void helper3(void *v1, void *v2, void *v3)
{
    varargFn(3, v2, v3, v1);
}

但是你将不得不修改你的 varargFn,这可能不值得这么麻烦。