GCC内联汇编的副作用

3
有人能用简单的语言解释一下GCC文档中“Clobbers and Scratch Registers”这一部分吗?下面是一个虚构的平方和指令,它需要两个指向内存中浮点值的指针,并产生一个浮点寄存器输出。请注意,在asm参数中,x和y都出现了两次,一次用于指定访问的内存,一次用于指定asm使用的基本寄存器。通常情况下,您不会浪费一个寄存器来做这件事,因为GCC可以将同一个寄存器用于两个目的。然而,如果在此asm中同时使用%1和%3表示x并期望它们相同,则是愚蠢的。实际上,%3可能不是一个寄存器,它可能是指向x所指对象的符号内存引用。
asm ("sumsq %0, %1, %2"
 : "+f" (result)
 : "r" (x), "r" (y), "m" (*x), "m" (*y));

这里是一个虚构的指令:*z++ = *x++ * *y++。请注意,由于汇编会修改x、y和z指针寄存器,因此必须将它们指定为输入/输出。
asm ("vecmul %0, %1, %2"
 : "+r" (z), "+r" (x), "+r" (y), "=m" (*z)
 : "m" (*x), "m" (*y));

在第一个示例中,将*x*y列在输入操作数中的目的是什么?同一文档还说明:

特别地,没有办法指定输入操作数被修改而不同时将其指定为输出操作数。

在第二个示例中,为什么要使用输入操作数部分?它的任何操作数都没有在汇编语句中使用。
作为奖励,如何更改以下示例,以便无需使用volatile关键字?(来源: SO 帖子)
void swap_2 (int *a, int *b)
{
int tmp0, tmp1;

__asm__ volatile (
    "movl (%0), %k2\n\t" /* %2 (tmp0) = (*a) */
    "movl (%1), %k3\n\t" /* %3 (tmp1) = (*b) */
    "cmpl %k3, %k2\n\t"
    "jle  %=f\n\t"       /* if (%2 <= %3) (at&t!) */
    "movl %k3, (%0)\n\t"
    "movl %k2, (%1)\n\t"
    "%=:\n\t"

    : "+r" (a), "+r" (b), "=r" (tmp0), "=r" (tmp1) :
    : "memory" /* "cc" */ );
}

提前致谢。我已经为此苦苦思索了两天。


2
我的猜测是,"m" (*x), "m" (*y) 被添加为输入操作数,以确保在调用扩展汇编模板之前将 x 和 y 的值实现到内存中。如果您将它们省略,并通过寄存器传递地址,则不能保证代码生成器实际上已将数据写入指向 2 个寄存器的 x 和 y 的内存中。 "m" (*x), "m" (*y) 输入约束确保在执行内联汇编之前,x 和 y 的值已经在内存中。这种情况可能会发生在某些代码结构和优化开启的情况下。 - Michael Petch
上面我的注释也同样适用于第二个例子(vecmul)。 - Michael Petch
如果虚构指令允许将内存操作数作为参数,那么这将不是一个问题。 - Michael Petch
交换代码非常低效,但从技术上讲,它本来就不需要 volatile 修饰符。实际上当优化并将 swap_2 嵌入其他函数时,有 volatile 会产生更少高效的代码。volatile 不需要是因为汇编模板的所有副作用都已通过输入、输出和 clobber 操作数进行了说明。 - Michael Petch
1
顺便提一下,在汇编模板中没有代码的情况下,严格只使用约束就可以交换两个输入的数据。这在这个答案中得到了证明:https://stackoverflow.com/a/39499595/3857942。 这是内联汇编的更高级使用。 - Michael Petch
显示剩余2条评论
1个回答

4
在第一个例子中,*x*y必须作为输入操作数列出,以便GCC知道指令的结果取决于它们。否则,GCC可能会将对*x*y的存储移动到内联汇编片段之后,而该片段将访问未初始化的内存。可以通过编译此示例来查看这一点:
double
f (void)
{
  double result;
  double a = 5;
  double b = 7;
  double *x = &a;
  double *y = &b;
  asm ("sumsq %0, %1, %2"
       : "+X" (result)
       : "r" (x), "r" (y) /*, "m" (*x), "m" (*y)*/);
  return result;
}

这导致:
f:
    leaq    -16(%rsp), %rax
    leaq    -8(%rsp), %rdx
    pxor    %xmm0, %xmm0
#APP
# 8 "t.c" 1
    sumsq %xmm0, %rax, %rdx
# 0 "" 2
#NO_APP
    ret

两个leaq指令只是将寄存器设置为指向未初始化的堆栈上的红区域。赋值语句已经消失了。

第二个示例也是如此。

我认为您可以使用同样的技巧来消除volatile。但我认为这并不是必要的,因为已经有一个"memory"清除操作,告诉GCC内联汇编从内存中读取或写入。


在交换代码中,根本不需要使用volatile。内联汇编的所有副作用都已经在约束条件中考虑到了。"memory" clobber 会确保在执行汇编模板之前,数据已经被写入内存中。 - Michael Petch
1
顺便说一句,volatile并不意味着您可以省略"memory" clobber。 asm volatile仅意味着它不是输入的纯函数,即使输出未使用,它也需要按照源代码所述运行多次,并且不能与其他asm volatile重新排序。 - Peter Cordes
@Florian Weimer非常感谢。您的示例解释得很好。出于好奇,您使用了哪些选项来获得这个干净的汇编代码? - listerreg
我删除了无关的行。但您可以使用“-fno-asynchronous-unwind-tables -O2 -S -o-”来实现几乎相同的效果。 - Florian Weimer
@Florian Weimer gcc -fno-stack-protector -fno-asynchronous-unwind-tables -O2 -S -o- 完成了任务。谢谢。 - listerreg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接