使变量参数函数callee清理

时间:2016-02-25 12:06:44

标签: c assembly x86 variadic-functions calling-convention

假设我有一个功能:

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;
}

被调用为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?

1 个答案:

答案 0 :(得分:2)

你可以做的是做一些辅助功能。每个辅助函数都需要固定数量的元素,并且选择要调用的辅助函数将在编译时完成。然后,每个辅助函数都会调用你的vararg函数。

每次调用将保存一条指令,代价是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    $3, %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    $5, %edi
        call    helper1
        movl    $5, %esi
        movl    $5, %edi
        call    helper2
        movl    $5, %edx
        movl    $5, %esi
        movl    $5, %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,这可能不值得。