我有两个函数,在C ++中是这样的:
void f1(...);
void f2(...);
我可以更改f1
的正文,但是f2
是在另一个我不能更改的库中定义的。我绝对必须在f2
内(尾)调用f1
,并且必须将提供给f1
的所有参数传递给f2
,但是据我所知,这在纯C或C ++中是不可能的。不幸的是,f2
没有其他选择接受va_list
。对f2
的调用发生在函数的最后,因此我需要某种形式的尾调用。
我决定使用汇编程序弹出当前函数的堆栈框架,然后跳转到f2
(它实际上是作为函数指针和变量接收的,因此这就是为什么我首先将其存储在寄存器中的原因):
__asm {
mov eax, f2
leave
jmp eax
}
在MSVC ++中,它似乎首先在Debug中起作用,但是它以某种方式与其他函数的返回值混淆,有时会崩溃。在Release中,它总是崩溃。
此汇编代码是否错误,还是对编译器进行了某些优化以某种方式破坏了此代码?
答案 0 :(得分:2)
在您进行挖掘时,编译器将不做任何保证。蹦床功能可能会起作用,但是您必须保存它们之间的状态,并进行大量挖掘。
这里是一个框架,但是您将需要了解很多有关调用约定,类方法调用等的信息。 /
* argn, ..., arg0, retaddr */
trampoline:
push < all volatile regs >
call <get thread local storage >
copy < volatile regs and ret addr > to < local storage >
pop < volatile regs >
remove ret addr
call f2
call < get thread local storage >
restore < volatile regs and ret addr>
jmp f1
ret
答案 1 :(得分:1)
为了确保安全,您必须用纯asm编写f1
。
在所有主要的x86调用约定中,被调用方“拥有” args,并且可以修改保存它们的堆栈空间。 (C源代码是否更改了它们,以及是否将它们声明为const
。)
例如如果在禁用优化的情况下进行编译,则void foo(int x) { x += 1; bar(x); }
可能会修改包含x
的返回地址上方的堆栈空间。再次调用具有相同参数的参数需要再次存储它们,除非您知道被调用方没有踩到它们。相同的参数适用于从一个函数的结尾进行尾调用。
我检查了on the Godbolt compiler explorer;实际上,MSVC和gcc都确实在调试版本中修改了堆栈上的x
。 gcc在推add DWORD PTR [ebp+8], 1
之前先使用[ebp+8]
。
实际上,编译器可能实际上没有利用可变参数函数,因此,根据函数的定义,如果您可以说服他们进行尾部调用,则可以不使用它
请注意,尽管void bar(...);
在C中不是有效的原型,但是:
# gcc -xc on Godbolt to force compiling as C, not C++
<source>:1:10: error: ISO C requires a named argument before '...'
它在C ++中有效,或者至少g ++接受而gcc则不接受。 MSVC在C ++模式but not in C mode中接受它。 (Godbolt具有完全独立的C模式,带有一组不同的编译器,您可以使用它们来使MSVC将代码编译为C而不是C ++。我不知道使用gcc方式将其翻转为C模式的命令行选项具有-xc
和-xc++
)
无论如何,在f2();
的末尾编写f1
可能会(在优化的版本中)起作用,但这很讨厌并且完全对编译器说谎通过了。当然,仅适用于没有注册参数的调用约定。 (但是您显示的是32位的asm,因此您很可能会使用没有注册参数的调用约定。)
在这种情况下,任何体面的编译器都将使用jmp f2
进行优化的尾调用,因为它们都返回void
。 (对于非无效,您将return f2();
)
顺便说一句,如果mov eax, f2
有效,那么jmp f2
也将有效。
但是,您的代码无法在优化的版本中工作,因为您假设编译器创建了旧的堆栈框架,并且该函数不会在任何地方内联。
即使在调试版本中也是不安全的,因为编译器可能已经push
调出了一些调用保留的寄存器,这些寄存器需要在离开函数之前(以及在运行leave
销毁堆栈框架之前被弹出)
@mevets显示的蹦床想法可以简化:如果args有合理的固定大小上限,则可以将传入的args中的64或128字节的潜在args复制到{{1}的args中}。一些SIMD向量可以做到这一点。然后,您可以正常调用f1
,然后从asm包装程序中调用f1
。
如果有潜在的注册参数,请将它们保存在复制的参数之前的堆栈空间中,并在进行尾调用之前将其还原。