【问题标题】:Make a variable argument function callee cleanup使变量参数函数被调用者清理
【发布时间】:2016-02-25 12:06:44
【问题描述】:

假设我有一个函数:

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?

【问题讨论】:

  • 你没有说哪个编译器,但我怀疑他们中的任何一个都可以被告知省略调用者清理。您最多可以指定调用约定,不幸的是,这会强制可变参数成为调用者清理。 AFAIK。
  • 这会破坏 ABI;影响比仅仅禁止使用 libc 更糟糕。 XY 问题?你真正想完成什么?
  • @Olaf 我处于内核模式,没有外部依赖项。这是一个内部函数,从未导出到其他任何地方。我试图在整个代码中记录各种事件,但它显着增加了代码大小 - 对于内核模式非分页代码,这是一个问题。 MSFT 在这里特别强加了一些非常严格的限制。
  • @EOF 在被调用方,您不能使用ret N,因为ret 指令需要立即操作数,true。但是,您可以执行 pop eax; add esp, N; push eax; ret; 之类的操作 - 基本上将返回地址放入寄存器中,根据需要更改堆栈大小,然后将返回地址推回并执行正常的 ret。这有点棘手,但它是可行的。

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


【解决方案1】:

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

您将在每次调用时节省一条指令,代价是 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生成一个简短的sn-p

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,这可能不值得麻烦。

【讨论】:

  • 使用void* 并不是一个普遍的好主意。在 32 位模式下,您可能想要打印不适合 void* 的 64 位值。在 64 位模式下,您可能想要打印一个未在同一寄存器中传递的浮点数。
  • 好点;您可能必须为您使用的每种类型组合提供辅助函数。只用 asm 编写它们没有帮助。
猜你喜欢
  • 2018-10-18
  • 2020-12-16
  • 1970-01-01
  • 1970-01-01
  • 2016-02-11
  • 2012-05-27
  • 2011-06-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多