我如何指示可以使用内联 ASM 参数*指向*的内存?

How can I indicate that the memory *pointed* to by an inline ASM argument may be used?

考虑以下小函数:

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
}

使用 gcc,this compiles to

foo:
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

请特别注意,对 iptriptr[10] = 1 的第一次写入根本不会发生:内联汇编 nop 是函数中的第一件事,并且仅2 的最终写入出现(在 ASM 调用之后)。显然,编译器决定它只需要提供 iptr 自身 值的最新版本,而不是它指向的内存。

我可以告诉编译器内存必须是最新的 memory 破坏,像这样:

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):"memory");
    iptr[10] = 2;
}

这会产生预期的代码:

foo:
        mov     DWORD PTR [rdi+40], 1
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

然而,这是一个太强的条件,因为它告诉编译器所有内存必须被写入。例如,在以下函数中:

void foo2(int* iptr, long* lptr) {
    iptr[10] = 1;
    lptr[20] = 100;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
    lptr[20] = 200;
}

期望的行为是让编译器优化掉对 lptr[20] 的第一次写入,而不是对 iptr[10] 的第一次写入。 "memory" 破坏器无法实现此目的,因为这意味着必须进行两次写入:

foo2:
        mov     DWORD PTR [rdi+40], 1
        mov     QWORD PTR [rsi+160], 100 ; lptr[10] written unecessarily
        nop
        mov     DWORD PTR [rdi+40], 2
        mov     QWORD PTR [rsi+160], 200
        ret

有没有什么方法可以告诉编译器接受 gcc 扩展 asm 语法,asm 的输入包括指针和它可以指向的任何东西?

没错;要求一个指针作为内联 asm 的输入确实 not 暗示 pointed-to 内存也是输入或输出或两者兼而有之。使用寄存器输入和寄存器输出,因为所有 gcc 都知道你的 asm 只是通过屏蔽低位来对齐指针,或者向它添加一个常量。 (在这种情况下,您会 想要 它来优化死掉的商店。)

简单的选项是 asm volatile"memory" clobber1.

您要求的更具体的方法是使用"dummy"内存操作数以及寄存器中的指针。您的 asm 模板不引用此操作数(除了可能在 asm 注释中以查看编译器选择的内容)。它告诉编译器您实际读取、写入或读取+写入的内存。

虚拟内存输入:"m" (*(const int (*)[]) iptr)
或输出:"=m" (*(int (*)[]) iptr)。或者当然 "+m" 使用相同的语法。

该语法正在转换为 pointer-to-array 并取消引用,因此实际输入是 C 数组。 (如果您实际上有一个数组,而不是指针,则不需要任何转换,只需将其作为内存操作数即可。)

如果使用 [] 未指定大小,则告诉 GCC 相对于该指针访问的任何内存都是输入、输出或 in/out 操作数。 如果你使用 [10][some_variable],那就告诉编译器具体的大小。对于 runtime-variable 大小,gcc 实际上错过了优化,即 iptr[size+1] 而不是 输入的一部分。

GCC documents this 所以支持。我认为如果数组元素类型与指针相同,或者如果它是 char.

,则不会违反 strict-aliasing

(from the GCC manual)
An x86 example where the string memory argument is of unknown length.

   asm("repne scasb"
    : "=c" (count), "+D" (p)
    : "m" (*(const char (*)[]) p), "0" (-1), "a" (0));

如果您可以避免在指针输入操作数上使用 early-clobber,虚拟内存输入操作数通常会选择使用相同寄存器的简单寻址模式。

但是如果您确实使用 early-clobber 来确保 asm 循环的严格正确性,有时伪操作数会使 gcc 浪费内存操作数基地址上的指令(和额外的寄存器)。检查编译器的 asm output


背景:

这是 inline-asm 示例中的一个普遍存在的错误,通常未被发现,因为 asm 被包装在一个函数中,该函数不会内联到任何调用者中,这些调用者会诱使编译器重新排序存储以进行合并 dead-store消除.

GNU C 内联 asm 语法是围绕向编译器描述 单个 指令而设计的。目的是告诉编译器有关带有 "m""=m" 操作数约束的内存输入或内存输出,它会选择寻址模式。

在内联 asm 中编写整个循环需要小心确保编译器真正知道发生了什么(或 asm volatile 加上一个 "memory" 破坏),否则你在更改周围代码时有损坏的风险,或启用允许 cross-file 内联的 link-time 优化。

另见 使用 asm 语句作为循环 body,仍在 C 中执行循环逻辑。实际 (non-dummy) "m""=m" 操作数,编译器可以通过在它选择的寻址模式中使用位移来展开循环。


脚注 1:"memory" 破坏程序使编译器将 asm 视为 non-inline 函数调用(可以读取或写入除 escape analysis 已证明的局部内存之外的任何内存没有逃脱)。逃逸分析包括 asm 语句本身的输入操作数,但也包括任何先前调用可能已将指针存储到其中的任何全局或静态变量。因此,通常本地循环计数器不必 spilled/reloaded 围绕 asm 语句和 "memory" 破坏。

asm volatile 是确保 asm 没有被优化的必要条件,即使它的输出操作数未被使用(因为您需要 un-declared 和 side-effect 写入内存).

或者对于仅由 asm 读取的内存,如果相同的输入缓冲区包含不同的输入数据,则需要 asm 再次 运行。如果没有 volatile,asm 语句可能会 CSEd 脱离循环。 (在考虑 asm 语句是否甚至需要 运行 时,"memory" 破坏器 不会 使优化器将所有内存视为输入。)

没有输出操作数的

asm 是隐式的 volatile,但最好将其显式化。 (GCC 手册有一节关于 asm volatile)。

例如asm("... sum an array ..." : "=r"(sum) : "r"(pointer), "r"(end_pointer) : "memory") 有一个输出操作数,因此不是隐式易变的。如果你喜欢

 arr[5] = 1;
 total += asm_sum(arr, len);
 memcpy(arr, foo, len);
 total += asm_sum(arr, len);

如果没有 volatile,第二个 asm_sum 可以优化掉,假设具有相同输入操作数(指针和长度)的相同 asm 将产生相同的输出。对于任何不是其显式输入操作数的纯函数的汇编,您都需要 volatile 。如果它没有优化,然后 "memory" 破坏将产生要求内存同步的预期效果。