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

时间:2019-06-03 17:49:23

标签: c gcc assembly clang inline-assembly

考虑以下小功能:

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的输入包括指针及其可以指向的任何内容?

1 个答案:

答案 0 :(得分:7)

是正确的;要求将指针作为内联asm的输入不是 not ,这并不意味着指向的内存也是输入或输出或两者兼有。通过寄存器输入和寄存器输出,所有gcc都知道您的asm只是通过掩盖低位来对齐指针,或者向其添加常数。 (在这种情况下,您将想要将其优化以消除无效存储。)

简单的选择是asm volatile"memory"遮盖物 1

您要使用的更窄更具体的方法是使用“虚拟”内存操作数以及寄存器中的指针。您的asm模板不会引用此操作数(除非可能在asm注释中以查看编译器选择了什么)。它告诉编译器您实际上读,写或读+写的内存。

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

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

如果未使用[]指定大小,则告诉GCC相对于该指针访问的任何内存都是输入,输出或输入/输出操作数。如果使用{ {1}}或[10],告诉编译器特定的大小。对于运行时可变大小,gcc实际上错过了[some_variable]不是输入的一部分的优化。

GCC documents this,因此支持它。我认为,如果数组元素类型与指针相同,或者如果是iptr[size+1],那不是严格混叠。

  

(来自GCC手册)
   一个x86示例,其中字符串内存参数的长度未知。

char

如果可以避免在指针输入操作数上使用早期指针,则伪存储器输入操作数通常将使用同一寄存器来选择简单的寻址模式。

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


背景:

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

GNU C内联汇编语法是围绕向编译器描述指令而设计的。目的是告诉编译器有关具有 asm("repne scasb" : "=c" (count), "+D" (p) : "m" (*(const char (*)[]) p), "0" (-1), "a" (0)); "m"操作数约束的内存输入或内存输出,并选择寻址方式。

在嵌入式asm中编写整个循环需要格外小心,以确保编译器确实知道发生了什么事情(或"=m"加上asm volatile破坏者),否则在更改周围的代码或启用时会冒着被破坏的风险。链接时优化,允许跨文件内联。

另请参阅Looping over arrays with inline assembly,以将"memory"语句用作循环 body ,但仍在C语言中执行循环逻辑。使用实际的(非虚拟的){{1} }和asm操作数,编译器可以通过在其选择的寻址模式下使用位移来展开循环。


脚注1:"m"破坏程序使编译器将asm视为非内联函数调用(可以读取或写入任何内存,但escape analysis证明没有逃脱的本地变量除外) 。转义分析包括asm语句本身的输入操作数,还包括任何先前调用可能已将指针存储到的全局或静态变量。因此,通常不必使用"=m"缓冲区在"memory"语句周围溢出/重新加载本地循环计数器。

asm是必要的,以确保即使未使用asm的输出操作数,也不会对其进行优化(因为您需要进行未声明的写入内存的副作用)。

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

没有输出操作数的

"memory"隐式地为asm,但是将其显式是一个好主意。 (GCC手册的asm volatile部分)。

例如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); 可以优化,假设具有相同输入操作数(指针和长度)的同一个asm将产生相同的输出。对于任何不是其显式输入操作数的纯函数的asm,都需要volatile。如果无法进行优化,则asm_sum破坏者会产生要求内存同步的预期效果。