为什么这个内联汇编不能为每条指令使用单独的asm volatile语句?

时间:2012-01-17 07:41:18

标签: c linux gcc assembly x86-64

对于以下代码:

long buf[64];

register long rrax asm ("rax");
register long rrbx asm ("rbx");
register long rrsi asm ("rsi");

rrax = 0x34;
rrbx = 0x39;

__asm__ __volatile__ ("movq $buf,%rsi");
__asm__ __volatile__ ("movq %rax, 0(%rsi);");
__asm__ __volatile__ ("movq %rbx, 8(%rsi);");

printf( "buf[0] = %lx, buf[1] = %lx!\n", buf[0], buf[1] );

我得到以下输出:

buf[0] = 0, buf[1] = 346161cbc0!

虽然应该是:

buf[0] = 34, buf[1] = 39!

任何想法为什么它不能正常工作,以及如何解决它?

3 个答案:

答案 0 :(得分:22)

你破坏了内存,但没有告诉GCC,所以GCC可以在buf汇总调用中缓存值。如果您想使用输入和输出,请告诉GCC所有内容。

__asm__ (
    "movq %1, 0(%0)\n\t"
    "movq %2, 8(%0)"
    :                                /* Outputs (none) */
    : "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
    : "memory");                     /* Clobbered */

您通常也希望让GCC处理大部分mov,寄存器选择等 - 即使您明确约束寄存器(rrax是stil %rax)让信息流经GCC或你会得到意想不到的结果。

__volatile__错了。

__volatile__存在的原因是,您可以保证编译器将您的代码准确放置在原来的位置......这是此代码的完全不必要的保证。实现内存屏障等高级功能是必要的,但如果只修改内存和寄存器,则几乎完全没用。

GCC已经知道它无法在printf之后移动此程序集,因为printf调用访问buf,并且buf可能被程序集破坏。 GCC已经知道它不能在rrax=0x39;之前移动程序集,因为rax是汇编代码的输入。那么__volatile__会给你带来什么?什么都没有。

如果您的代码在没有__volatile__的情况下无效,那么代码中的错误应该是已修复,而不是仅添加__volatile__,并希望这会使一切变得更好。 __volatile__关键字不是魔术,不应该这样对待。

替代方案:

原始代码需要__volatile__吗?不。只需正确标记输入和符号值。

/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
   The inputs and clobbered values are specified.  There is no output
   so that section is blank.  */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");

为什么__volatile__对您没有帮助:

rrax = 0x34; /* Dead code */

GCC完全有权完全删除上述行,因为上述问题中的代码声称它从不使用rrax

更清晰的例子

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)");
}

反汇编或多或少与您在-O0

所期望的一样
movl $5, %rax
movq %rax, (global)

但是如果关闭优化,你可能会对装配很邋..我们试试-O2

movq %rax, (global)

糟糕! rax = 5;去了哪里?这是死代码,因为%rax从未在函数中使用 - 至少就GCC而言。海湾合作委员会没有偷看内部装配。删除__volatile__后会发生什么?

; empty

好吧,你可能会认为__volatile__正在通过让GCC放弃你宝贵的装配来为你服务,但它只是掩盖了GCC认为你的装配不是任何东西的事实。 GCC认为你的程序集不需要输入,不产生输出,并且没有内存。你最好把它理顺:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}

现在我们得到以下输出:

movq %rax, (global)

更好。但是如果你告诉GCC输入信息,它会确保%rax首先正确初始化:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}

输出,优化:

movl $5, %eax
movq %rax, (global)

正确!我们甚至不需要使用__volatile__

为什么__volatile__存在?

__volatile__的主要正确用法是,如果汇编代码除了输入,输出或破坏内存之外还执行其他操作。也许它与GCC不了解或影响IO的特殊寄存器相混淆。你在Linux内核中看到了很多,但它在用户空间中经常被滥用。

__volatile__关键字非常诱人,因为我们C程序员经常喜欢认为我们已经用汇编语言进行了几乎编程。不是。 C编译器进行了大量的数据流分析 - 因此您需要向编译器解释汇编代码的数据流。这样,编译器就可以安全地操纵你的程序集块,就像它操纵它生成的程序集一样。

如果您发现自己经常使用__volatile__,可以在汇编文件中编写整个函数或模块。

答案 1 :(得分:4)

编译器使用寄存器,它可以覆盖你放入它们的值。

在这种情况下,编译器可能在rbx赋值之后和内联汇编部分之前使用rrbx寄存器。

通常,您不应期望寄存器在内联汇编代码序列之后和之间保留其值。

答案 2 :(得分:2)

稍微偏离主题但我想跟进gcc内联汇编。

__volatile__的(非)需求来自GCC 优化内联汇编的事实。 GCC检查汇编语句的副作用/先决条件,如果发现它们不存在,它可能会选择移动汇编指令,甚至决定删除它。所有__volatile__所做的就是告诉编译器"停止关怀并将其放在那里"。

这通常不是你真正想要的。

这就是需要约束的地方。名称重载并实际用于GCC内联汇编中的不同内容:

  • 约束指定asm()
  • 中使用的输入/输出操作数
  • 约束指定" clobber列表",详细说明" state" (寄存器,条件代码,内存)受asm()
  • 的影响
  • 约束指定操作数类(寄存器,地址,偏移量,常量,......)
  • 约束声明汇编器实体和C / C ++变量/表达式之间的关联/绑定

在许多情况下,开发人员滥用 __volatile__因为他们发现他们的代码要么被移动,要么在没有它的情况下消失。如果发生这种情况,通常情况下,开发人员已经尝试告诉GCC有关装配的副作用/先决条件。例如,这个错误的代码:

register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;

asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);

它有几个错误:

  • 一,它只是由于gcc bug(!)而编译。通常,要在内联汇编中写入寄存器名称,需要双%%,但如果您实际指定它们,则会得到编译器/汇编程序错误/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
  • 第二,它没有告诉编译器何时何地需要/使用变量。相反,它假设编译器从字面上尊重asm()。对于Microsoft Visual C ++可能是这样,但对于gcc而言不是

如果您编译而不进行优化,则会创建:

0000000000400524 <main>:
[ ... ]
  400534:       b8 d2 04 00 00          mov    $0x4d2,%eax
  400539:       bb e1 10 00 00          mov    $0x10e1,%ebx
  40053e:       48 01 c3                add    %rax,%rbx
  400541:       48 89 da                mov    %rbx,%rdx
  400544:       b8 5c 06 40 00          mov    $0x40065c,%eax
  400549:       48 89 d6                mov    %rdx,%rsi
  40054c:       48 89 c7                mov    %rax,%rdi
  40054f:       b8 00 00 00 00          mov    $0x0,%eax
  400554:       e8 d7 fe ff ff          callq  400430 <printf@plt>
[...]
您可以找到add指令以及两个寄存器的初始化,并打印出预期的寄存器。另一方面,如果您进行优化,则会发生其他情况:
0000000000400530 <main>:
  400530:       48 83 ec 08             sub    $0x8,%rsp
  400534:       48 01 c3                add    %rax,%rbx
  400537:       be e1 10 00 00          mov    $0x10e1,%esi
  40053c:       bf 3c 06 40 00          mov    $0x40063c,%edi
  400541:       31 c0                   xor    %eax,%eax
  400543:       e8 e8 fe ff ff          callq  400430 <printf@plt>
[ ... ]
您对&#34;二手&#34;的初始化寄存器不再存在。编译器放弃了它们,因为没有任何东西可以看到它们正在使用它们,并且它保留了汇编指令,它在之前使用了两个变量。它就在那里,但它没有做任何事情(幸运的是......如果rax / rbx 一直在使用谁可以告诉他们发生了什么。 ..)。

原因是你实际上告诉 GCC,程序集正在使用这些寄存器/这些操作数值。这没有任何意义使用volatile,但所有事实都与您使用无约束asm()表达式有关。

正确执行 的方式是通过约束,即您使用:

int foo = 1234;
int bar = 4321;

asm("add %1, %0" : "+r"(bar) : "r"(foo));
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);

这告诉编译器程序集:

  1. 在寄存器"+r"(...)中有一个参数,它们都需要在汇编语句之前初始化,并由汇编语句修改,并将变量bar与它相关联。
  2. 在寄存器中有第二个参数,"r"(...)需要在汇编语句之前初始化,并且被声明视为readonly / not modified。在此处,将foo与此相关联。
  3. 注意没有指定寄存器赋值 - 编译器根据编译的变量/状态选择它。以上的(优化的)输出:

    0000000000400530 <main>:
      400530:       48 83 ec 08             sub    $0x8,%rsp
      400534:       b8 d2 04 00 00          mov    $0x4d2,%eax
      400539:       be e1 10 00 00          mov    $0x10e1,%esi
      40053e:       bf 4c 06 40 00          mov    $0x40064c,%edi
      400543:       01 c6                   add    %eax,%esi
      400545:       31 c0                   xor    %eax,%eax
      400547:       e8 e4 fe ff ff          callq  400430 <printf@plt>
    [ ... ]
    GCC内联汇编约束几乎总是必要以某种形式或另一种形式
    ,但可能有多种可能的方式来描述对编译器的要求相同;而不是上述,你也可以写:

    asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
    

    这告诉gcc:

    1. 该语句有一个输出操作数,即变量bar,在语句中找到后,"=r"(...)
    2. 该语句有一个输入操作数,即变量foo,它将放入寄存器"r"(...)
    3. 操作数零也是输入操作数,并使用bar
    4. 进行初始化

      或者,又一个替代方案:

      asm("add %1, %0" : "+r"(bar) : "g"(foo));
      

      告诉gcc:

      1. bla (哈欠 - 与之前相同,bar输入/输出)
      2. 该语句有一个输入操作数,即变量foo,该语句并不关心它是在寄存器中,在内存中还是在编译时常量中(&#39;是"g"(...)约束)
      3. 结果与前者不同:

        0000000000400530 <main>:
          400530:       48 83 ec 08             sub    $0x8,%rsp
          400534:       bf 4c 06 40 00          mov    $0x40064c,%edi
          400539:       31 c0                   xor    %eax,%eax
          40053b:       be e1 10 00 00          mov    $0x10e1,%esi
          400540:       81 c6 d2 04 00 00       add    $0x4d2,%esi
          400546:       e8 e5 fe ff ff          callq  400430 <printf@plt>
        [ ... ]
        因为现在,GCC 实际上已经找到了 foo 是一个编译时常量只需将值嵌入 add 指令即可!那不是很整洁吗?

        不可否认,这很复杂,需要习惯。优点是让编译器选择哪些寄存器用于哪些操作数允许整体优化代码;例如,如果在宏和/或static inline函数中使用内联汇编语句,则编译器可以根据调用上下文在代码的不同实例中选择不同的寄存器。或者,如果某个值在一个地方是编译时可评估/常量而在另一个地方没有,则编译器可以为其定制创建的程序集。

        将GCC内联装配约束视为&#34;扩展函数原型&#34; - 它们告诉编译器参数/返回值的类型和位置,以及更多。如果你没有指定这些约束,你的内联汇编就会创建仅对全局变量/状态进行操作的函数模拟 - 正如我们可能都认为的那样,它们很少完全按照你的意图行事。