【问题标题】:What is the difference between 'asm', '__asm' and '__asm__'?'asm'、'__asm' 和 '__asm__' 有什么区别?
【发布时间】:2011-03-20 09:39:37
【问题描述】:

据我所知,__asm { ... };__asm__("..."); 之间的唯一区别是第一个使用mov eax, var,第二个使用movl %0, %%eax,最后使用:"=r" (var)。还有哪些不同之处?那么asm呢?

【问题讨论】:

标签: c gcc assembly visual-c++ inline-assembly


【解决方案1】:

MSVC 内联汇编和 GNU C 内联汇编之间存在巨大差异。 GCC 语法旨在优化输出而不会浪费指令,用于包装单个指令或其他东西。 MSVC 语法被设计得相当简单,但是如果没有延迟和额外的指令,就无法使用 AFAICT,因为您的输入和输出需要在内存中往返。

如果您出于性能原因使用内联 asm,这使得 MSVC 内联 asm 仅在您完全用 asm 编写整个循环时才可行,而不是用于将短序列包装在内联函数中。下面的示例(用函数包装 idiv)是 MSVC 不擅长的事情:~8 个额外的存储/加载指令。

MSVC 内联汇编(MSVC 使用,可能还有 icc,也可能在一些商业编译器中可用):

  • 查看您的 asm 以确定您的代码在哪个寄存器上运行。
  • 只能通过内存传输数据。例如,在寄存器中存在的数据由编译器存储以准备您的mov ecx, shift_count。因此,使用编译器不会为您生成的单个 asm 指令涉及在进出内存的过程中往返。
  • 对初学者更友好,但通常无法避免输入/输出数据的开销。即使除了语法限制之外,当前版本的 MSVC 中的优化器也不擅长围绕内联 asm 块进行优化。

GNU C 内联汇编is not a good way to learn asm。您必须非常了解 asm,以便您可以告诉编译器您的代码。你必须了解编译器需要知道什么。该答案还具有其他内联汇编指南和问答的链接。 标签 wiki 通常有很多关于 asm 的好东西,但只是指向 GNU 内联 asm 的链接。 (该答案中的内容也适用于非 x86 平台上的 GNU 内联汇编。)

gcc、clang、icc 以及一些实现 GNU C 的商业编译器使用 GNU C 内联 asm 语法:

  • 你必须告诉编译器你破坏了什么。不这样做会导致周围代码以非显而易见的难以调试的方式被破坏。
  • 功能强大但难以阅读、学习和使用语法来告诉编译器如何提供输入以及在哪里找到输出。例如"c" (shift_count) 将让编译器在您的内联汇编运行之前将 shift_count 变量放入 ecx
  • 对于大块代码来说特别笨重,因为 asm 必须在字符串常量内。所以你通常需要

    "insn   %[inputvar], %%reg\n\t"       // comment
    "insn2  %%reg, %[outputvar]\n\t"
    
  • 非常无情/更难,但允许较低的开销,尤其是。用于包装单个指令。 (包装单个指令是最初的设计意图,这就是为什么你必须特别告诉编译器早期的clobbers 以阻止它在出现问题时使用相同的寄存器进行输入和输出。)


示例:全角整数除法 (div)

在 32 位 CPU 上,将 64 位整数除以 32 位整数,或进行全乘 (32x32->64),可以从内联汇编中受益。 gcc 和 clang 没有利用 idiv 代替 (int64_t)a / (int32_t)b,可能是因为如果结果不适合 32 位寄存器,指令就会出错。所以不像this Q&A about getting quotient and remainder from one div,这是一个内联汇编的用例。 (除非有办法通知编译器结果适合,所以 idiv 不会出错。)

我们将使用调用约定将一些 args 放入寄存器中(hi 甚至在 right 寄存器中),以显示更接近于内联 tiny像这样的函数。


MSVC

在使用 inline-asm 时要小心 register-arg 调用约定。显然,inline-asm 支持的设计/实现非常糟糕,以至于the compiler might not save/restore arg registers around the inline asm, if those args aren't used in the inline asm。感谢@RossRidge 指出这一点。

// MSVC.  Be careful with _vectorcall & inline-asm: see above
// we could return a struct, but that would complicate things
int _vectorcall div64(int hi, int lo, int divisor, int *premainder) {
    int quotient, tmp;
    __asm {
        mov   edx, hi;
        mov   eax, lo;
        idiv   divisor
        mov   quotient, eax
        mov   tmp, edx;
        // mov ecx, premainder   // Or this I guess?
        // mov   [ecx], edx
    }
    *premainder = tmp;
    return quotient;     // or omit the return with a value in eax
}

更新:显然在eaxedx:eax 中留下了一个值,然后从非空函数(没有returnis supported, even when inlining 的末尾脱落。。我认为这仅在asm 语句之后没有代码时才有效。请参阅Does __asm{}; return the value of eax? 这避免了输出的存储/重新加载(至少对于quotient),但我们对输入无能为力。在带有堆栈参数的非内联函数中,它们已经在内存中,但在这个用例中,我们正在编写一个可以有用内联的小函数。


使用 MSVC 19.00.23026 /O2 on rextester 编译(带有用于查找 exe 目录的 main()dumps the compiler's asm output to stdout)。

## My added comments use. ##
; ... define some symbolic constants for stack offsets of parameters
; 48   : int ABI div64(int hi, int lo, int divisor, int *premainder) {
    sub esp, 16                 ; 00000010H
    mov DWORD PTR _lo$[esp+16], edx      ## these symbolic constants match up with the names of the stack args and locals
    mov DWORD PTR _hi$[esp+16], ecx

    ## start of __asm {
    mov edx, DWORD PTR _hi$[esp+16]
    mov eax, DWORD PTR _lo$[esp+16]
    idiv    DWORD PTR _divisor$[esp+12]
    mov DWORD PTR _quotient$[esp+16], eax  ## store to a local temporary, not *premainder
    mov DWORD PTR _tmp$[esp+16], edx
    ## end of __asm block

    mov ecx, DWORD PTR _premainder$[esp+12]
    mov eax, DWORD PTR _tmp$[esp+16]
    mov DWORD PTR [ecx], eax               ## I guess we should have done this inside the inline asm so this would suck slightly less
    mov eax, DWORD PTR _quotient$[esp+16]  ## but this one is unavoidable
    add esp, 16                 ; 00000010H
    ret 8

有大量额外的 mov 指令,编译器甚至无法优化其中的任何一条。我想它可能会看到并理解内联汇编中的mov tmp, edx,并将其作为premainder 的存储。但我猜这需要将premainder 从堆栈加载到内联 asm 块之前的寄存器中。

使用_vectorcall 的这个函数实际上更糟糕,而不是使用普通的everything-on-the-stack ABI。在寄存器中有两个输入,它将它们存储到内存中,因此内联 asm 可以从命名变量中加载它们。如果这是内联的,则更多的参数可能会在 regs 中,并且必须将它们全部存储起来,因此 asm 将具有内存操作数!因此,与 gcc 不同的是,内联它并没有太多好处。

在 asm 块内执行 *premainder = tmp 意味着更多的代码是用 asm 编写的,但确实避免了其余部分的完全脑死的存储/加载/存储路径。这将指令总数减少了 2 条,降至 11 条(不包括 ret)。

我试图从 MSVC 中获得最好的代码,而不是“使用错误”并创建一个稻草人论据。但是 AFAICT 包装非常短的序列是可怕的。 大概有一个 64/32 -> 32 除法的内在函数允许编译器为这种特殊情况生成好的代码,所以在 MSVC 上为此使用内联 asm 的整个前提可能是一个稻草人参数强>。但它确实向您表明,对于 MSVC,内在函数比内联 asm 好得多


GNU C (gcc/clang/icc)

当内联 div64 时,Gcc 甚至比此处显示的输出更好,因为它通常可以安排前面的代码首先在 edx:eax 中生成 64 位整数。

我无法让 gcc 为 32 位 vectorcall ABI 进行编译。 Clang 可以,但它在带有"rm" 约束的内联 asm 上很糟糕(在 Godbolt 链接上尝试:它通过内存反弹函数 arg 而不是在约束中使用 register 选项)。 64 位 MS 调用约定接近 32 位向量调用,前两个参数在 edx、ecx 中。不同之处在于在使用堆栈之前还有 2 个参数进入 regs(并且被调用者不会将 args 从堆栈中弹出,这就是 MSVC 输出中的 ret 8 的含义。)

// GNU C
// change everything to int64_t to do 128b/64b -> 64b division
// MSVC doesn't do x86-64 inline asm, so we'll use 32bit to be comparable
int div64(int lo, int hi, int *premainder, int divisor) {
    int quotient, rem;
    asm ("idivl  %[divsrc]"
          : "=a" (quotient), "=d" (rem)    // a means eax,  d means edx
          : "d" (hi), "a" (lo),
            [divsrc] "rm" (divisor)        // Could have just used %0 instead of naming divsrc
            // note the "rm" to allow the src to be in a register or not, whatever gcc chooses.
            // "rmi" would also allow an immediate, but unlike adc, idiv doesn't have an immediate form
          : // no clobbers
        );
    *premainder = rem;
    return quotient;
}

compiled with gcc -m64 -O3 -mabi=ms -fverbose-asm。使用 -m32,您只需获得 3 个负载、idiv 和一个商店,正如您可以从更改该 Godbolt 链接中的内容中看到的那样。

mov     eax, ecx  # lo, lo
idivl  r9d      # divisor
mov     DWORD PTR [r8], edx       # *premainder_7(D), rem
ret

对于 32 位向量调用,gcc 会做类似的事情

## Not real compiler output, but probably similar to what you'd get
mov     eax, ecx               # lo, lo
mov     ecx, [esp+12]          # premainder
idivl   [esp+16]               # divisor
mov     DWORD PTR [ecx], edx   # *premainder_7(D), rem
ret   8

MSVC 使用 13 条指令(不包括 ret),而 gcc 的 4 条。正如我所说,使用内联,它可能只编译为一条,而 MSVC 仍可能使用 9 条。(它不需要保留堆栈空间或加载premainder;我假设它仍然必须存储3个输入中的大约2个。然后它在asm中重新加载它们,运行idiv,存储两个输出,然后在asm之外重新加载它们。所以这是4加载/存储用于输入,另外 4 个用于输出。)

【讨论】:

  • 我想说微软的内联汇编不太宽容。它从来都不是很可靠,而且新的编译器版本是否不会破坏您的代码总是有点废话。显然,对于微软来说,这也是一个巨大的维护噩梦,试图确保新的编译器版本不会破坏现有代码,这就是为什么他们放弃了对他们最近支持的架构(IA-64、AMD64、ARM)的支持。特别是您的 MSVC 示例很危险,因为它将基于寄存器的调用约定与内联汇编混合在一起:msdn.microsoft.com/en-us/library/k1a8ss06.aspx.
  • @PeterCordes Yah,对于您的具体示例,它应该可以工作。问题存在的事实证明了内联汇编器在微软的编译器中是多么粗暴地实现。我认为编译器甚至不会警告您您正在使用它需要的其他寄存器。
  • 物有所值,here's how I would write the function in inline asm for MSVC。底部显示拆卸。我用__stdcall 编译了这个函数,但是__cdecl 也同样有效。不要使用__vectorcall__fastcall,它们只会悲观内联汇编。请注意,输出与 GCC 的输出几乎相同,除了需要将参数从堆栈显式加载到寄存器中。这是 MSVC 内联汇编的硬性限制,完全无法避免并且不可避免地会导致次优代码。
  • 请注意,整个业务确实被编译器、内联 asm 和所有内容内联。 Here's an example of that, with a stub function that exercises it。同样,唯一可怕的是参数传递 in 到块的方式。如果你写得正确,你可以避免对块的返回值 out 的所有惩罚。当然,编译器不会解析内联 asm 指令,因此即使 Test 函数从不使用它,它也没有足够的知识来忽略余数的设置。但我也不确定 GCC 是否会这样做。
  • 是的,完全支持和完全优化。现在,如果你要我出示一些支持这种说法的文件……我必须四处寻找。没有什么我可以指出的。我的大部分知识来自于实际编写代码、调整代码、分析反汇编,以及在具有广泛的正确性验证断言的项目中实际使用它。但我认为它曾经发出警告但不再发出警告的事实是令人信服的证据,表明它正在识别成语。不难看出它是怎么做到的,因为所有的调用约定都是这样工作的。
【解决方案2】:

您使用哪一种取决于您的编译器。这不像 C 语言那样标准。

【讨论】:

  • 嗯,是的。这些领先的下划线的全部意义在于表明这是非标准的。
  • @StevenSudit 不,前导下划线表示“保留”,见wiki.sei.cmu.edu/confluence/display/c/…
  • @Sapphire_Brick:特别是保留给实现的使用,这意味着当你看到它们时,你就知道其中涉及到特定于实现的行为。
  • 这个答案应该是评论,实际上不是答案
  • @AyberkÖzgür:OP 想知道使用哪一个。答案是他在这件事上没有选择,他必须使用他的编译器支持的那个。不能在 Visual C++ 中使用 GCC 内联 asm,也不能在 gcc 中使用 MSVC 内联 asm。
【解决方案3】:

asm 与 GCC 中的__asm__

asm 不适用于-std=c99,您有两种选择:

  • 使用__asm__
  • 使用-std=gnu99

更多详情:error: ‘asm’ undeclared (first use in this function)

__asm 与 GCC 中的__asm__

我找不到记录 __asm 的位置(特别是在 https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Alternate-Keywords.html#Alternate-Keywords 中未提及),但从 GCC 8.1 source 来看,它们完全相同:

  { "__asm",        RID_ASM,    0 },
  { "__asm__",      RID_ASM,    0 },

所以我只使用记录在案的__asm__

【讨论】:

  • OP 询问__asm { ... };(带大括号,而不是括号),所以它绝对不是 GNU C 内联汇编。 Clang 和 MSVC 支持 MSVC 样式的内联 asm,但 gcc 不支持。
  • @PeterCordes 感谢您提供的信息。部分是因为 GCC 标签,部分是因为谷歌命中理论。现在查看历史,OP 没有添加 GCC 标签,但我也没有添加 ;-)
  • @PeterCordes 除了gcc 之外,您还可以在问题中添加适当的微软编译器标签吗?谢天谢地,我不知道是哪一个:-)
  • visual-c++是微软C++方言的标签,由MSVC实现。
【解决方案4】:

使用 gcc 编译器,差别不大。 asm__asm__asm__ 是相同的,它们只是用于避免冲突命名空间的目的(有用户定义的函数,命名为 asm 等)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-01-05
    • 2021-08-30
    • 2010-10-02
    • 2011-12-12
    • 2010-09-16
    • 2012-03-14
    • 2012-02-06
    相关资源
    最近更新 更多