有什么方法可以在 linux 平台上编译微软风格的 inline-assembly 代码吗?

Is there any way to complie a microsoft style inline-assembly code on a linux platform?

如标题中所述,我想知道是否有任何方法可以在 linux OS(例如 ubuntu).

_asm{
    mov edi, A;
    ....
    EMMS;
}

示例代码是 inline-assembly 代码的一部分,可以在 win10 上使用 cl.exe 编译器成功编译。有什么办法可以在 linux 上编译它吗?我是否必须以 GNU c/c++ 风格重写它(即 __asm__{;;;})?

首先,您通常应该替换内联汇编(使用内部函数或纯 C)而不是移植它。 https://gcc.gnu.org/wiki/DontUseInlineAsm


clang -fasm-blocks 主要兼容 MSVC 低效的内联 asm 语法。但它不支持通过将值留在 EAX 中然后从非空函数的末尾掉落来返回值。

所以你必须编写内联汇编,将值放入一个命名的 C 变量中,return,通常会导致额外的 store/reload 使 MSVC 语法更糟。 (非常糟糕,除非你在 asm 中编写一个完整的循环来分摊将数据输入/输出 asm 块的 store/reload 开销)。请参阅 What is the difference between 'asm', '__asm' and '__asm__'? 比较 MSVC inline-asm 在包装单个指令时的效率有多低。当这些函数不内联时,它在带有堆栈参数的函数内部就不那么愚蠢了,但是只有当你已经使事情变得低效时才会发生这种情况(例如,使用遗留的 32 位调用约定而不使用 link-time 优化来内联小功能)。

MSVC 可以将 A 内联到调用方时立即替换为 1,但 clang 不能。两者都击败了常量传播,但 MSVC 至少避免了通过 store/reload 反弹常量输入。 (只要您只将它与可以支持立即源操作数的指令一起使用。)

Clang 接受 __asmasm__asm__ 来引入 asm 块。 MSVC 接受 __asm(2 个下划线,如 clang)或 _asm(更常用,但 clang 不接受)。

因此,对于现有的 MSVC 代码,您可能需要 #define _asm __asm,这样您的代码就可以同时使用 MSVC 和 clang 进行编译,除非您无论如何都需要制作单独的版本。或者使用clang -D_asm=asm在命令行设置一个CPP宏

示例:使用 MSVC 或 clang -fasm-blocks

编译

(不要忘记启用优化:clang -fasm-blocks -O3 -march=native -flto -Wall。如果您想要一个可以在 earlier/other CPU 上 运行 而不是编译的二进制文件,请省略或修改 -march=native主持人。)

int a_global;

inline
long foo(int A, int B, int *arr) {
    int out;
    // You can't assume A will be in RDI: after inlining it prob. won't be
    __asm {
        mov   ecx, A                   // comment syntax
        add   dword ptr [a_global], 1
        mov   out, ecx
    }
    return out;
}

使用 x86-64 Linux clang 8.0 on Godbolt 编译表明 clang 可以内联包含内联汇编的包装函数,以及需要多少 store/reload MSVC 语法(相对于可以接受输入和输出的 GNU C 内联汇编在寄存器中)。

我在 Intel 语法 asm 输出模式下使用 clang,但它在 AT&T 语法模式下输出时也会编译 Intel 语法 asm 块。 (通常 clang 会直接编译为机器代码,它也可以正确地编译。)

## The x86-64 System V ABI passes args in rdi, rsi, rdx, ...
# clang -O3 -fasm-blocks -Wall
foo(int, int, int*):
        mov     dword ptr [rsp - 4], edi        # compiler-generated store of register arg to the stack

        mov     ecx, dword ptr [rsp - 4]        # start of inline asm
        add     dword ptr [rip + a_global], 1
        mov     dword ptr [rsp - 8], ecx        # end of inline asm

        movsxd  rax, dword ptr [rsp - 8]        # reload `out` with sign-extension to long (64-bit) : compiler-generated
        ret

注意编译器如何用 [rsp - 4][rsp - 8] 替换 asm 源代码块中的 C 局部变量 Aout。并且静态存储中的变量获得 RIP 相对寻址。 GNU C 内联汇编不会这样做,您需要声明 %[name] 操作数并告诉编译器将它们放在哪里。

我们甚至可以看到 clang 将该函数两次内联到一个调用者中,并将符号扩展优化为 64 位,因为该函数仅 returns int.

int caller() {
    return foo(1, 2, nullptr) + foo(1, 2, nullptr);
}
caller():                             # @caller()
        mov     dword ptr [rsp - 4], 1

        mov     ecx, dword ptr [rsp - 4]      # first inline asm
        add     dword ptr [rip + a_global], 1
        mov     dword ptr [rsp - 8], ecx

        mov     eax, dword ptr [rsp - 8]     # compiler-generated reload
        mov     dword ptr [rsp - 4], 1       # and store of A=1 again

        mov     ecx, dword ptr [rsp - 4]      # second inline asm
        add     dword ptr [rip + a_global], 1
        mov     dword ptr [rsp - 8], ecx

        add     eax, dword ptr [rsp - 8]     # compiler-generated reload
        ret

所以我们可以看到,仅从内联 asm 中读取 A 会导致优化失败:编译器会再次存储 1,即使 asm 只读取该输入而没有修改它。

我还没有做过分配或读取 a_global before/between/after asm 语句的测试,以确保编译器 "knows" 该变量被 asm 语句修改。

我也没有测试将指针传递到 asm 块并遍历指向的数组,看看它是否像 GNU C 内联 asm 中的 "memory" 破坏。我想是的。

My Godbolt link 还包括一个从 EAX 中的值的非空函数末尾掉落的示例。这是由 MSVC 支持的,但是当内联到调用者时,UB 就像往常一样用于 clang 和 breaks。 (奇怪的是没有警告,即使在 -Wall)。您可以在上面的我的 Godbolt link 上看到 x86 MSVC 是如何编译它的。


https://gcc.gnu.org/wiki/DontUseInlineAsm

将 MSVC asm 移植到 GNU C 内联 asm 几乎肯定是错误的选择。 编译器对优化内在函数的支持非常好,因此您通常可以让编译器为您生成优质高效的 asm。

如果您要对现有的手写 asm 做任何事情,通常用纯 C 替换它们将是最有效的,当然也是最有前途的前进道路。将来可以自动向量化为更宽向量的代码总是好的。但是,如果您确实需要手动矢量化一些棘手的改组,那么除非编译器以某种方式将其弄得一团糟,否则内在函数是可行的方法。

查看您从内在函数中获得的编译器生成的 asm,以确保它与原始函数一样好或更好。

如果您正在使用 MMX EMMS,现在可能是用 SSE2 内在函数替换您的 MMX 代码的好时机。 SSE2 是 x86-64 的基线,很少 Linux 系统是 运行ning 过时的 32 位内核。

Is there any way to complie a microsoft style inline-assembly code on a linux platform?

是的,这是可能的。有点。

对于 GCC,您必须同时使用 Intel 和 AT&T 语法。由于问题 24232,它不适用于 Clang,Inline assembly operands don't work with .intel_syntax and Issue 39895, Error: unknown token in expression using inline asm.

这是模式。汇编程序模板使用 .intel_syntax。然后,在您的 asm 模板的末尾,您切换回 .att_syntax 模式,以便它处于编译器生成的其余 asm.

的正确模式
#include <cstddef>
int main(int argc, char* argv[])
{
    size_t ret = 1, N = 0;
    asm __volatile__
    (
        ".intel_syntax   noprefix ;\n"
        "xor esi, esi    ;\n"           // zero RSI
        "neg %1          ;\n"           // %1 is replaced with the operand location chosen by the compiler, in this case RCX
        "inc %1          ;\n"
        "push %1         ;\n"           // UNSAFE: steps on the red-zone
        "pop rax         ;\n"
        ".att_syntax     prefix ;\n"
        : "=a" (ret)      // output-only operand in RAX
          "+c" (N)        // read-write operand in RCX
        :                 // no read-only inputs
        : "%rsi"          // RSI is clobbered: input and output register constraints can't pick it
    );
    return (int)ret;
}

如果您使用任何内存操作数,这将不起作用,因为编译器会将 AT&T 语法 4(%rsp) 替换为模板而不是 [rsp + 4],例如。

这也仅在您使用gcc -masm=intel编译时有效。否则,当 GCC 发出 Intel 语法时,您会将汇编器置于 AT&T 模式。因此,使用 .intel_syntax noprefix 会破坏您在 GCC 中使用任一语法的能力。


mov edi, A;

我帮助编写的代码不像您显示的那样在汇编程序中使用变量。我不知道它与 Intel 风格的 ASM 配合得有多好(不好?)。我知道不支持 MASM 风格语法。

您可以使用 asmSymbolicNames 来完成。有关详细信息,请参阅 GCC Extended ASM HowTo

但是,要转换为 GCC 可以使用的内容,您只需要使用位置参数:

__asm__ __volatile__
(
    ".intel_syntax   noprefix ;\n"
    "mov edi, %0     \n";            // inefficient: use a "D" constraint instead of a mov
    ...
    ".att_syntax     prefix ;\n"
    : : "r" (A) : "%edi"
);

或者更好的是,首先使用 "D" 约束来请求 EDI / RDI 中的变量。如果 GNU C 内联 asm 语句以 mov 开头或结尾,这通常表明您做错了。


关于 asmSymbolicNames,以下是 GCC Extended ASM HowTo 对它们的评价:

This code makes no use of the optional asmSymbolicName. Therefore it references the first output operand as %0 (were there a second, it would be %1, etc). The number of the first input operand is one greater than that of the last output operand. In this i386 example, that makes Mask referenced as %1:

uint32_t Mask = 1234;
uint32_t Index;

  asm ("bsfl %1, %0"
     : "=r" (Index)
     : "r" (Mask)
     : "cc");

That code overwrites the variable Index (‘=’), placing the value in a register (‘r’). Using the generic ‘r’ constraint instead of a constraint for a specific register allows the compiler to pick the register to use, which can result in more efficient code. This may not be possible if an assembler instruction requires a specific register.

The following i386 example uses the asmSymbolicName syntax. It produces the same result as the code above, but some may consider it more readable or more maintainable since reordering index numbers is not necessary when adding or removing operands. The names aIndex and aMask are only used in this example to emphasize which names get used where. It is acceptable to reuse the names Index and Mask.

uint32_t Mask = 1234;
uint32_t Index;

  asm ("bsfl %[aMask], %[aIndex]"
     : [aIndex] "=r" (Index)
     : [aMask] "r" (Mask)
     : "cc");

The sample code is part of a inline-assembly code which can be compiled successfully on win10 with cl.exe compiler...

回到 10,000 英尺,如果您正在寻找易于使用的东西来像在 Microsoft 环境中一样集成内联 ASM,那么 Linux 上没有它。 GCC 内联 ASM 绝对糟糕。 GCC 内联汇编器是一个古老的、难以使用的工具,我讨厌与之交互。

(而且您还没有经历过带有虚假线路信息的难以理解的错误消息)。

Peter 的想法解决了我的问题。我刚刚在我的源文件中添加了一个宏,其中所有函数都由一个大的 inline-asm 英特尔语法块组成。宏如下所示:

#define _asm\
        asm(".intel_syntax noprefix\n");\
        asm\

之后我用命令编译它:

clang++ -c -fasm-blocks source.cpp

那就一切OK了。