【发布时间】: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 是一个优先事项。
【问题讨论】:
-
你没有说哪个编译器,但我怀疑他们中的任何一个都可以被告知省略调用者清理。您最多可以指定调用约定,不幸的是,这会强制可变参数成为调用者清理。 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