用 Intrinsics 替换 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
作为帧指针编译,并且没有其他需要恢复的寄存器)。它看起来很脆弱,否则它只在你根本不需要内联汇编的情况下才有用。
不是returning,而是跳转到uAddr
,相当于打tailcall。
没有用于任意跳转或操作堆栈的内在函数。如果你需要那个,那你就不走运了。单独询问这个片段是没有意义的,只有在有足够的上下文才能看到它是如何被使用的。即哪个 return 地址在堆栈上很重要,还是可以将其编译为 call/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');
}
它也可以很好地编译 x86-64 和 32 位 x86 MSVC,gcc/clang 32 位和 64 位 on the Godbolt compiler explorer。 (以及非 x86 架构)。这是 MSVC 的 32 位 asm 输出,因此您可以与您为讨厌的包装函数获得的结果进行比较。你可以看到它基本上将有用的部分(mov eax, uAddr
/ jmp
或 call
)内联到调用者中。
;; 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
。 linker 可以在 link 时间计算从调用站点到绝对目标的相对偏移量,因此编译器没有理由使用寄存器间接 call
.
如果我们不告诉编译器创建与位置无关的代码,那么在这种情况下,它是一个有用的优化来解决相对于代码的绝对地址,jumps/calls。
在 32 位代码中,每个可能的目标地址都在每个可能的源地址的范围内,但在 64 位代码中更难。 在 32 位模式下,clang 确实发现了这个优化!但即使在 32 位模式下,MSVC 和 gcc 也会错过它。
我用 gcc/clang 玩了一些东西:
// don't use
oCMOB * CreateNewInstance(void) asm("0x00718590");
有点作品,但只是作为一个完整的 hack。 Gcc 只是使用那个字符串,就好像它是一个符号一样,所以它将 call 0x00718590
提供给 assembler,后者会正确处理它(生成一个绝对重定位,link 在非-PIE 可执行文件)。但是对于 -fPIE
,我们发出 0x00718590@GOTPCREL
作为符号名称,所以我们搞砸了。
当然,在 64 位模式下,PIE 可执行文件或库将超出该绝对地址的范围,因此无论如何只有非 PIE 才有意义。
另一个想法是用绝对地址在 asm 中定义符号,并提供一个原型,让 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
linktest
、从Godbolt, using the binary output to see how it assembled+linked:
编译的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 定义为具有该数值的 assemble 时间符号。哎呀。所以它不太像带有那个地址的标签,就像你用 .org 0x718590; CNI:
.
得到的那样
但是因为我们只能在非 PIE 可执行文件中使用 rel32 call
,所以这是可以的,除非你用 -no-pie
编译但忘记了 -fno-pie
,在这种情况下你就完蛋了. :/
提供一个带有符号定义的单独目标文件可能有效。
Clang 似乎完全按照我们的意愿行事,即使使用 -fPIE,其内置 assembler。此机器代码只能 link 与 -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 rel32
比 mov imm32
差。)使用 -m32 -fPIE
,它甚至 assemble。
我接手了一个不活跃的项目并且已经在其中修复了很多东西,但是我没有正确地获得 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
作为帧指针编译,并且没有其他需要恢复的寄存器)。它看起来很脆弱,否则它只在你根本不需要内联汇编的情况下才有用。
不是returning,而是跳转到uAddr
,相当于打tailcall。
没有用于任意跳转或操作堆栈的内在函数。如果你需要那个,那你就不走运了。单独询问这个片段是没有意义的,只有在有足够的上下文才能看到它是如何被使用的。即哪个 return 地址在堆栈上很重要,还是可以将其编译为 call/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');
}
它也可以很好地编译 x86-64 和 32 位 x86 MSVC,gcc/clang 32 位和 64 位 on the Godbolt compiler explorer。 (以及非 x86 架构)。这是 MSVC 的 32 位 asm 输出,因此您可以与您为讨厌的包装函数获得的结果进行比较。你可以看到它基本上将有用的部分(mov eax, uAddr
/ jmp
或 call
)内联到调用者中。
;; 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
。 linker 可以在 link 时间计算从调用站点到绝对目标的相对偏移量,因此编译器没有理由使用寄存器间接 call
.
如果我们不告诉编译器创建与位置无关的代码,那么在这种情况下,它是一个有用的优化来解决相对于代码的绝对地址,jumps/calls。
在 32 位代码中,每个可能的目标地址都在每个可能的源地址的范围内,但在 64 位代码中更难。 在 32 位模式下,clang 确实发现了这个优化!但即使在 32 位模式下,MSVC 和 gcc 也会错过它。
我用 gcc/clang 玩了一些东西:
// don't use
oCMOB * CreateNewInstance(void) asm("0x00718590");
有点作品,但只是作为一个完整的 hack。 Gcc 只是使用那个字符串,就好像它是一个符号一样,所以它将 call 0x00718590
提供给 assembler,后者会正确处理它(生成一个绝对重定位,link 在非-PIE 可执行文件)。但是对于 -fPIE
,我们发出 0x00718590@GOTPCREL
作为符号名称,所以我们搞砸了。
当然,在 64 位模式下,PIE 可执行文件或库将超出该绝对地址的范围,因此无论如何只有非 PIE 才有意义。
另一个想法是用绝对地址在 asm 中定义符号,并提供一个原型,让 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
linktest
、从Godbolt, using the binary output to see how it assembled+linked:
反汇编
# 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 定义为具有该数值的 assemble 时间符号。哎呀。所以它不太像带有那个地址的标签,就像你用 .org 0x718590; CNI:
.
但是因为我们只能在非 PIE 可执行文件中使用 rel32 call
,所以这是可以的,除非你用 -no-pie
编译但忘记了 -fno-pie
,在这种情况下你就完蛋了. :/
提供一个带有符号定义的单独目标文件可能有效。
Clang 似乎完全按照我们的意愿行事,即使使用 -fPIE,其内置 assembler。此机器代码只能 link 与 -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 rel32
比 mov imm32
差。)使用 -m32 -fPIE
,它甚至 assemble。