繁体   English   中英

使用Intrinsics替换内联汇编尾部调用函数epilogue for x86 / x64 msvc

[英]replace inline assembly tailcall function epilogue with Intrinsics for x86/x64 msvc

我接受了一个非活动项目,并且已经修复了很多,但是我没有正确地使用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编译为帧指针,而不需要其他需要恢复的寄存器)。 它看起来很脆弱,或者它只在你根本不需要内联asm的情况下才有用。

它不是返回,而是跳转到uAddr ,这相当于进行uAddr

任意跳转或操纵堆栈都没有内在函数。 如果你需要,那你就不走运了。 单独询问这个片段是没有意义的,只有足够的上下文才能看到它是如何被使用的。 即,返回地址在堆栈上是否重要,或者它是否可以编译为调用/ ret而不是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');
}

它还可以在Godbolt编译器资源管理器上使用x86-64和32位x86 MSVC以及gcc / clang 32和64位进行编译 (还有非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中定义符号,并提供一个原型,让gcc只能直接使用它,没有@PLT或通过GOT。 (我可能已经为func() asm("0x...");也使用隐藏的可见性进行破解。)

我只是在用“隐藏”属性进行黑客攻击后才意识到这在位置无关的代码中是无用的,所以你不能在共享库或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

编译链接的gcc输出为+拆卸testGodbolt,使用二进制输出看看它是如何组装+联

 # 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: .org 0x718590; CNI: .

但是因为我们只能在非PIE可执行文件中使用rel32 call ,所以这是可以的,除非你用-no-pie编译但是忘了-fno-pie ,在这种情况下你搞砸了。 :/

提供带有符号定义的单独目标文件可能有效。

Clang看起来完全符合我们的要求,即使是-fPIE,也有内置的汇编程序。 这个机器代码只能与-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 ,它甚至不会组装。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM