在c++中使用asm代码操作寄存器时会发生什么?

What happens to registers when you manipulate them using asm code in C++?

本文关键字:什么 寄存器 操作 c++ 代码 asm      更新时间:2023-10-16

部分代码:

int x = 1;
for(int i = 1; i < 10; i++)
{
    x *= i;
    __asm {
        mov eax, x 
    };
}

如果这个程序使用eax来增加i的值,当我操作eax时会发生什么?

编译器是否会保存__asm调用之前的寄存器,并在asm代码执行后使用它们,还是会忽略eax被操纵并继续产生某种奇怪的行为?

exx内部发生了什么?

EDIT:即使我的代码只适用于Visual c++,我想知道一般情况下会发生什么,以及不同的编译器将如何处理这一点。

exx内部发生了什么?

内联asm既不神奇也不特殊。c++编译器已经将c++翻译成asm。

您的内联asm只是包含在编译器生成的asm中。如果你想了解真正发生了什么,看看编译器的asm输出,看看你的代码是如何适应的。

。这部分问题的答案是,它取决于编译器、上下文和优化选项,所以你应该自己看看生成的asm。


你的问题使用msvc风格的内联asm,它保存/恢复内联asm周围的寄存器(除了ebp,当然还有esp)。所以我认为你的例子永远不会有效果。msvc风格没有语法来向编译器传达关于寄存器使用的任何信息,或者在寄存器中获取值而不是在内存中获取值。

您有时会看到MSVC内联asm在int函数结束时在eax中留下一个值,而没有return语句,在有限的情况下,编译器不会对内联asm结束和函数结束之间的eax做任何事情。


你在评论中说你想要一个g++的答案,它甚至不能编译你的例子,但无论如何,我将为你写一个。

GNU C内联asm使用不同的语法,这需要您告诉编译器哪些寄存器是输入(不修改),哪些是读写或仅写。

这取决于程序员使用约束向编译器正确描述asm,否则你会踩到它的脚趾。使用GNU C内联asm就像跳舞一样,你可以潜在地获得好的结果,但是如果你不小心,你会踩到编译器的脚趾。(此外,通常编译器可以自己生成很好的asm,而内联asm是一种非常脆弱的优化方式;其中一个主要问题是,内联之后的常量传播不可能通过内联asm实现。


无论如何,让我们用一个GNU C内联asm的工作示例来尝试一下:

int foo_badasm(int n) {
  int factorial = 1;
  for (int i=1 ; i < n ; i++ ) {
    // compile with -masm=intel, since I'm using Intel syntax here
    asm volatile ("mov   eax, %[x]   # THIS LINE FROM INLINE ASMn"
                  "# more linesn"
                  // "xor  eax,eaxn"
        : // no outputs, making the volatile implicit even if we didn't specify it
        : [x] "rmi" (factorial)   // input can be reg, memory, or immediate
        : // "eax"   // uncomment this to tell the compiler we clobber eax, so our asm doesn't break step on the compiler's toes.
    );
    factorial *= i;
  }
  return factorial;
}

请参阅Godbolt编译器资源管理器上的asm输出代码,以及没有asm语句的相同函数。

内部循环编译成这样(gcc 6.1 -O3 -fverbose-asm -masm=intel, -fno-tree-vectorize保持简单)。你也可以用clang试试。

.L10:
    mov   eax, eax   # THIS LINE FROM INLINE ASM    # <retval>
    imul    eax, edx        # <retval>, i
    add     edx, 1    # i,
    cmp     edi, edx  # n, i
    jne     .L10      #,
    ret

可以看到,在这种情况下,内联asm语句产生了一个no-op。(mov eax,eaxrax截断为32位,将上面的32位归零,但在这个函数中它们已经是零了。)

如果我们不做其他事情,比如将寄存器置零,或者从其他来源获取mov,我们就会破坏函数。编译器生成的asm只依赖于 asm语句中列出的约束,而不依赖于代码的文本(不像MSVC)。


查看内联汇编标签wiki获取更多信息,特别是关于MSVC和GNU C内联汇编的区别的答案。

更重要的是,在实际使用内联asm之前,请阅读https://gcc.gnu.org/wiki/DontUseInlineAsm。

简短的回答:"你脚……别开枪!"

如果你使用asm{}块,C/c++期望你知道你在做什么。它不会生成任何代码来"准备"您的插入,也不会"在插入之后进行清理",不会知道或考虑您所做的操作。在这种情况下,你会搬起石头砸自己的脚。如果您做了编译器没有预料到的、不知道的事情,并且可能会干扰编译器生成的代码正在做的事情,那么您就引入了一个您自己造成的bug。

如果你打算在汇编代码中操作寄存器,你必须采取所有适当的步骤来保持和恢复这些寄存器的状态。你必须知道你在做什么

恕我直言,最好的方法是不使用内联ASM,而是在ASM中编写一个单独的函数。

通常的步骤是:

  1. 用C或c++编写代码并使其正常运行
  2. 打印编译器生成的程序集清单。
  3. 使用编译器生成的汇编代码作为基础(这使得编写调用和返回代码更容易)。

请注意,编写汇编语言来优化编译器生成的代码通常是浪费开发时间。你应该在优化之前进行分析。

另外,编写内联汇编程序是不可移植的。