确保 clang-cl 中自定义 ASM 函数的 x64 合规性

问题描述 投票:0回答:1

对于我自定义编译的本机 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 项目页面设置)。

我尝试过的事情:

  • 手动修改RSP,使用子RSP,32。这会导致建立帧指针寄存器,因为编译器会将其视为动态堆栈分配。这增加了太多的开销,以至于不值得首先使用静态编译的函数
  • 同样,我可以直接在 asm (mov rbx,pState) 中引用“pState”,这将导致添加影子空间 - 而且,pState 随后将被复制到堆栈上,并从该堆栈位置加载到 rbx 中,而不是寄存器。这再次违背了我在这里所做的事情的目的。
  • 在 asm 块之后直接调用“pAddress”作为函数指针。这仍然不会导致代码生成有任何差异
  • 使用普通asm(),或扩展asm,与“attribute((naked))”结合使用。这不会生成序言/尾声,我可以自己编写 - 但随后展开信息丢失了。 clang-cl 似乎不理解任何展开数据指令,例如 .allocstack 或 .pushreg,导致“错误:未知指令” - 无论使用哪种类型的 asm 块。

是否有任何原因导致影子空间丢失,以及有什么方法可以在不添加任何不必要的开销(如帧指针)的情况下将其到达那里(同时仍然具有展开信息)?我也愿意接受其他建议 - 例如,如果有一些内在的东西让我设置这些寄存器(同时仍然编译为一步),我就不需要使用汇编(操作具有全局效果的特定寄存器是我无法编写纯 C++ 的主要原因)。

c++ x86-64 inline-assembly clang-cl
1个回答
0
投票

所以,我按照 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 的微小成本是可以接受的。

© www.soinside.com 2019 - 2024. All rights reserved.