跳转/尾调用到另一个功能

时间:2018-10-30 15:48:33

标签: c++ assembly x86 inline-assembly

我有两个函数,在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中,它总是崩溃。

此汇编代码是否错误,还是对编译器进行了某些优化以某种方式破坏了此代码?

2 个答案:

答案 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

如果有潜在的注册参数,请将它们保存在复制的参数之前的堆栈空间中,并在进行尾调用之前将其还原。