对于我自定义编译的本机 x64 JIT 代码,我有某些固有函数。其中很多只是从我的代码中调用的,因此我将使用自己的编译器生成。然而,其中一些是直接从 c++ 代码调用的,因此我希望将它们编译在静态库内,这样它们至少可以静态链接(如果不是内联的话)。
我需要对这些函数使用内联汇编,因为它们执行无法用常规 C++ 表达的操作,例如从函数输入设置非易失性寄存器。但是,函数本身的行为必须类似于常规 x64 函数 - 它需要序言/结尾,并且必须具有必要的展开信息来支持堆栈跟踪和异常处理。因此我无法使用 MSVC(这是我的本机编译器),因此我决定在 Visual Studio 中使用 clang-cl 创建静态库。到目前为止我得到的最好的结果如下:
void interruptEntry(void* pState, const char* pAddress)
{
__asm
{
// load state into RBX
mov rbx,rcx
// load callstack-top into RDI
mov rax,[rbx]
mov rdi,[rax]
// call address
call rdx
};
}
这将生成正确的序言、尾声和所有必需的展开信息。然而,它严重缺乏 x64 所需的 32 字节影子空间(需要调用 pAddress):
Acclimate Engine.dll!interruptEntry(void *, const char *):
push rdi
push rbx
mov rbx,rcx
mov rax,qword ptr [rbx]
mov rdi,qword ptr [rax]
call rdx
pop rbx
pop rdi
ret
请记住,虽然此代码是通过 clang-cl 生成的,但 DLL 是与 MSVC 链接的。 static-lib 使用 O2 编译(从 VisualStudio 项目页面设置)。
我尝试过的事情:
是否有任何原因导致影子空间丢失,以及有什么方法可以在不添加任何不必要的开销(如帧指针)的情况下将其到达那里(同时仍然具有展开信息)?我也愿意接受其他建议 - 例如,如果有一些内在的东西让我设置这些寄存器(同时仍然编译为一步),我就不需要使用汇编(操作具有全局效果的特定寄存器是我无法编写纯 C++ 的主要原因)。
所以,我按照 Margaret Bloom 的建议查看了 MASM64,我不得不说,在 MSVC 中仍然可以选择生成程序集通常很酷。但是,就我自己的情况而言,我评估了列表中的所有函数,以便可能在汇编中重写。 大多数都是更复杂的函数(包括调用其他函数;断言;循环,跳转;...),但只需要一些特定的命令来修改寄存器,或者跳转到一个地址而不是调用。因此,虽然我可以将 MASM 用于更简单的情况,但我确实希望能够选择使用内联汇编来编码某些内容,并将其添加到普通的 c++ 函数中。幸运的是,我确实找到了方法:
void interruptEntry(ExecutionStateJIT& state, Func pAddress)
{
asm("mov rbx,%0;"
:
: "r" (&state)
: "%rbx");
auto* pTemp = *((void**)&state);
asm("mov rdi,[%0];"
:
: "r" (pTemp)
: "%rdi");
pAddress();
asm volatile("");
}
这符合我的预期结果:
push rdi
push rbx
sub rsp,28h
mov rbx,rcx
mov rax,qword ptr [rcx]
mov rdi,qword ptr [rax]
call rdx
nop
add rsp,28h
pop rbx
pop rdi
ret
看起来使用扩展内联汇编确实是一种方法。使用 __asm 块会以某种方式使编译器混淆正在进行的调用类型 - 这很遗憾,因为我更喜欢 __asm 的语法。扩展 asm 确实允许我告诉编译器哪些寄存器被修改 - 这很巧妙,因为我想要移植的某些函数,我明确不希望修改的寄存器被停止。我也可以混合使用常规 C++,这是我对某些操作的要求之一。
我还必须确保在调用后包含一个空的易失性 asm 块,以防止它进行尾部调用(这会弹出之前的寄存器)。有一个nop,我不确定(它不是由空块直接引起的;就好像我在那里写了一个易失性nop,它会有两个nop)。但我有点假设 clang 知道它在这里做什么 - 否则,就我而言,一个 nop 的微小成本是可以接受的。