GCC 内联汇编的副作用

Posted

技术标签:

【中文标题】GCC 内联汇编的副作用【英文标题】:GCC Inline Assembly side effects 【发布时间】:2017-10-25 22:20:13 【问题描述】:

有人可以向我解释(换句话说)来自 GCC doc 的以下部分:

这是一个虚构的平方和指令,它采用两个指向内存中浮点值的指针并产生一个浮点寄存器输出。请注意,x 和 y 在 asm 参数中都出现了两次,一次指定访问的内存,一次指定 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 会修改它们。

asm ("vecmul %0, %1, %2"
 : "+r" (z), "+r" (x), "+r" (y), "=m" (*z)
 : "m" (*x), "m" (*y));

在第一个示例中,在输入操作数中列出 *x*y 有什么意义?同一文档指出:

特别是,如果不将输入操作数指定为输出操作数,就无法指定它们被修改。

在第二个示例中,为什么要使用输入操作数部分?无论如何,它的任何操作数都不会在汇编语句中使用。

作为奖励,如何将this SO 帖子中的以下示例更改为不需要volatile 关键字?

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" */ );

提前致谢。我现在为此苦苦挣扎了两天。

【问题讨论】:

我的猜测是 "m" (*x), "m" (*y) 被添加为输入操作数,以确保在调用扩展程序集模板之前将 x 和 y 的值实现到内存中。如果您不使用它们,并且通过寄存器传递地址,则无法保证代码生成器实际上将数据写入 x 和 y 的内存(由 2 个寄存器指向两个)。 "m" (*x), "m" (*y) 输入约束确保在执行内联汇编之前 x 和 y 的值在内存中。这种情况可能会在某些代码结构和优化处于启用状态时发生。 上面的 Mu 注释也适用于第二个示例 (vecmul)。 如果允许虚构指令将内存操作数作为参数,这将不是问题。 交换代码效率非常低,但从技术上讲,它甚至不需要 volatile 修饰符。事实上,它可能会产生效率较低的代码,并且在优化时存在 volatile 并且将 swap_2 内联到其他函数中。不需要 volatile,因为程序集模板的所有副作用都通过输入、输出和 clobber 操作数来考虑。 在旁注中,实际上可以交换两个输入的数据而无需在程序集模板内使用任何代码,并严格使用约束来完成工作。这在这个答案中得到了证明: ***.com/a/39499595/3857942 。那是内联汇编的更高级使用。 【参考方案1】:

在第一个示例中,*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" clobber,它告诉 GCC 内存是从内联汇编读取或写入的。

【讨论】:

在交换代码中volatile 根本不需要。内联汇编的所有副作用都包含在约束中。 "memory" clobber 将确保在执行程序集模板之前将数据实现到内存中。 顺便说一句,volatile 意味着您可以省略 "memory" clobber。 asm volatile 只是意味着它不是输入的纯函数,即即使输出未使用,它也需要运行多次,而不是与其他 asm volatile 重新排序。 @Florian Weimer 非常感谢。你的例子很好地解释了它。出于好奇,您使用了哪些选项来获得这个干净的 asm 代码? 我删除了不相关的行。但是您可以使用-fno-asynchronous-unwind-tables -O2 -S -o- 获得几乎相同的效果。 @Florian Weimer gcc -fno-stack-protector -fno-asynchronous-unwind-tables -O2 -S -o- 完成了这项工作。谢谢。

以上是关于GCC 内联汇编的副作用的主要内容,如果未能解决你的问题,请参考以下文章

GCC 内联汇编到 IAR 内联汇编

GCC 扩展内联汇编简介

在 gcc 中访问内联汇编中的字符串的地址

VC内联汇编和GCC内联汇编的语法区别

操作系统学习之GCC内联汇编

GCC内联汇编常见陷阱