c++ - 用内在的x86/x64 msvc替换内联程序尾调用函数结尾

标签 c++ x86 inline-assembly

我参加了一个不 Activity 的项目,并且已经在其中进行了很多修复,但是我无法正确替换Intrinsics来使用内联程序集,而x86 / x64 msvc编译器不再支持该内联程序集。

#define XCALL(uAddr)  \
__asm { mov esp, ebp }   \
__asm { pop ebp }        \
__asm { mov eax, uAddr } \
__asm { jmp eax }

用例:
static oCMOB * CreateNewInstance() {
    XCALL(0x00718590);
}

int Copy(class zSTRING const &, enum zTSTR_KIND const &) {
    XCALL(0x0046C2D0);
}

void TrimLeft(char) {
    XCALL(0x0046C630);
}

最佳答案

此代码段位于函数的底部(该函数不能内联,并且必须使用ebp作为帧指针进行编译,并且没有其他需要还原的寄存器)。它看起来很脆弱,否则仅在根本不需要内联汇编的情况下才有用。

它没有返回,而是跳转到uAddr,等效于进行尾调用。

没有用于任意跳转或操纵堆栈的内在函数。如果需要的话,您就不走运了。仅凭足够的上下文来了解如何使用此片段本身就没有意义。即哪个返回地址在堆栈上很重要,还是可以编译为调用/返回而不是jmp到该地址? (有关将其用作函数指针的简单示例,请参见此答案的第一个版本。)

从更新开始,用例只是为绝对函数指针包装的一种非常笨拙的方法。

相反,我们可以定义正确类型的static const函数指针,因此不需要包装器,并且编译器可以在使用它们的任何地方直接调用。 static const是我们让编译器知道它可以完全内联函数指针的方式,并且如果不需要,则不需要将它们存储为数据,就像普通的static const int xyz = 2;一样

struct oCMOB;
class zSTRING;
enum zTSTR_KIND { a, b, c };  // enum forward declarations are illegal

// C syntax
//static oCMOB* (*const CreateNewInstance)() = (oCMOB *(*const)())0x00718590;

// C++11
static const auto CreateNewInstance = reinterpret_cast<oCMOB *(*)()>(0x00718590);
// passing an enum by const-reference is dumb.  By value is more efficient for integer types
static const auto Copy = reinterpret_cast<int (*)(class zSTRING const &, enum zTSTR_KIND const &)>(0x0046C2D0);
static const auto TrimLeft = reinterpret_cast<void (*)(char)> (0x0046C630);

void foo() {
    oCMOB *inst = CreateNewInstance();
    (void)inst; // silence unused warning

    zSTRING *dummy = nullptr;  // work around instantiating an incomplete type
    int result = Copy(*dummy, c);
    (void) result;

    TrimLeft('a');
}

使用x86-64和32位x86 MSVC以及gcc / clang 32和64位on the Godbolt compiler explorer也可以很好地进行编译。 (以及非x86体系结构)。这是MSVC的32位asm输出,因此您可以将其与讨厌的包装器函数获得的输出进行比较。您可以看到它基本上是将有用的部分(mov eax, uAddr / jmpcall)内联到调用方中。
;; x86 MSVC -O3
$T1 = -4                                                ; size = 4
?foo@@YAXXZ PROC                                        ; foo
        push    ecx
        mov     eax, 7439760                          ; 00718590H
        call    eax

        lea     eax, DWORD PTR $T1[esp+4]
        mov     DWORD PTR $T1[esp+4], 2       ; the by-reference enum
        push    eax
        push    0                             ; the dummy nullptr
        mov     eax, 4637392                          ; 0046c2d0H
        call    eax

        push    97                                  ; 00000061H
        mov     eax, 4638256                          ; 0046c630H
        call    eax

        add     esp, 16                             ; 00000010H
        ret     0
?foo@@YAXXZ ENDP

对于重复调用相同的函数,编译器会将函数指针保留在保留调用的寄存器中。

由于某些原因,即使使用32位的位置相关代码,我们也无法直接获得call rel32。链接器可以在链接时计算从调用站点到绝对目标的相对偏移,因此编译器没有理由使用寄存器间接call

如果我们不告诉编译器创建与位置无关的代码,则在这种情况下,这是一个有用的优化,用于针对跳转/调用寻址相对于代码的绝对地址。

在32位代码中,每个可能的目标地址都在每个可能的源地址的范围内,但在64位中则更加困难。 在32位模式下,clang会发现此优化!但是即使在32位模式下,MSVC和gcc也会错过它。

我用gcc / clang玩了一些东西:
// don't use
oCMOB * CreateNewInstance(void) asm("0x00718590");

种类繁多,但只能作为一种技巧。 Gcc只是使用该字符串,就好像它是一个符号一样,因此它将call 0x00718590馈送给汇编器,该汇编器可以正确处理它(生成绝对重定位,可以在非PIE可执行文件中很好地链接)。但是,使用-fPIE,我们发出0x00718590@GOTPCREL作为符号名,因此很麻烦。

当然,在64位模式下,PIE可执行文件或库将超出该绝对地址的范围,因此无论如何,只有非PIE才有意义。

另一个想法是用一个绝对地址在asm中定义符号,并提供一个原型(prototype),使gcc仅在不使用@PLT或不通过GOT的情况下直接使用它。 (对于func() asm("0x..."); hack,我也可以使用隐藏的可见性来完成。)

我只有在使用“hidden”属性对其进行修改后才意识到,这在与位置无关的代码中没有用,因此无论如何您都不能在共享库或PIE可执行文件中使用它。
extern "C"不是必需的,但意味着我不必在嵌入式asm中搞乱名称处理。
#ifdef __GNUC__

extern "C" {
    // hidden visibility means that even in a PIE executable, or shared lib,
    // calls will go *directly* to that address, not via the PLT or GOT.
    oCMOB * CNI(void) __attribute__((__visibility__("hidden")));
}
//asm("CNI = 0x718590");  // set the address of a symbol, like `org 0x71... / CNI:`
asm(".set CNI, 0x718590");  // alternate syntax for the same thing


void *test() {
    CNI();    // works

    return (void*)CNI;  // gcc: RIP+0x718590 instead of the relative displacement needed to reach it?
    // clang appears to work
}
#endif

Godbolt, using the binary output to see how it assembled+linked 拆卸test的已编译+链接的gcc输出:
 # gcc -O3  (non-PIE).  Clang makes pretty much the same code, with a direct call and mov imm.
 sub    rsp,0x8
 call   718590 <CNI>
 mov    eax,0x718590
 add    rsp,0x8
 ret    

使用-fPIE,gcc + gas发出lea rax,[rip+0x718590] # b18ab0 <CNI+0x400520>,即它使用绝对地址作为RIP的偏移量,而不是减去。我猜这是因为gcc确实会发出lea CNI(%rip),%rax,并且我们已经将CNI定义为具有该数值的汇编时间符号。哎呀。因此,它不太像带有.org 0x718590; CNI:的带有该地址的标签。

但是,由于我们只能在非PIE可执行文件中使用rel32 call,因此可以这样做,除非您使用-no-pie进行编译,而忘记了-fno-pie,在这种情况下您很费劲。 :/

为单独的目标文件提供符号定义可能会起作用。

但是,即使使用-fPIE及其内置的汇编程序,Clang似乎也可以完全满足我们的要求。该机器码只能与-fno-pie链接(Godbolt的默认设置,而不是许多发行版的默认设置)。
 # disassembly of clang -fPIE machine-code output for test()
 push   rax
 call   718590 <CNI>
 lea    rax,[rip+0x3180b3]        # 718590 <CNI>
 pop    rcx
 ret    

因此,这实际上是安全的(但是次优,因为lea rel32mov imm32差。)对于-m32 -fPIE,它甚至不会汇编。

关于c++ - 用内在的x86/x64 msvc替换内联程序尾调用函数结尾,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52010509/

相关文章:

c++ - 我应该如何格式化我的 .dat 文件以便制作 3D vector 图?

gcc - 实际使用的Intel x86 0x2E/0x3E前缀分支预测吗?

linux - 在内联 GNU 汇编器中获取字符串长度

c++ - 具有多个文件夹的 Visual Studio 项目

c++ - Boost.Statechart - 记录的选择点方法问题

c++ - 在 OpenCV 中结合两个仿射变换矩阵

c - LEA EAX,[EAX] 有什么意义?

linux - 解释修补/保护 POP SS 后跟#BP 中断 (INT3) 的 Linux 提交消息

c - 在 gcc 的内联 asm 中使用 printf 函数

c - 使用 gcc 中的内联汇编从 stdin 扫描并打印到 stdout