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

标签 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根本不会发生:内联asmnop是函数中的第一件事,只有最后一次写入2才会出现(在asm调用之后)。显然,编译器决定只需要提供iptr本身值的最新版本,而不需要提供它指向的内存。
我可以告诉编译器内存必须是带有memoryblobber的最新版本,如下所示:
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"blobber无法实现这一点,因为它意味着必须同时进行两次写入:
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的输入并不意味着指向内存的指针也是一个输入或输出,或者两者都是。对于寄存器输入和寄存器输出,因为gcc知道asm只是通过屏蔽低位来对齐指针,或者向其添加常量。(在这种情况下,您希望它优化一个死掉的存储。)
简单的选择是asm volatile和a"memory"blobber1。
你要求的更具体的方法是使用“伪”内存操作数以及寄存器中的指针。您的asm模板没有引用此操作数(除了在asm注释中查看编译器选择了什么之外)。它告诉编译器您实际读、写或读+写哪个内存。
虚拟内存输入:"m" (*(const int (*)[]) iptr)
或输出:"=m" (*(int (*)[]) iptr)。当然也可以用同样的语法。
该语法将转换为指向数组的指针并取消引用,因此实际输入是一个C数组。(如果实际上有一个数组,而不是指针,则不需要任何转换,只需将其作为内存操作数请求即可。)
如果使用"+m"保留未指定的大小,则表示gcc相对于该指针访问的任何内存都是输入、输出或输入/输出操作数。如果使用[][10],则会告诉编译器特定的大小。对于运行时变量大小,gcc实际上忽略了[some_variable]不是输入一部分的优化。
GCC documents this因此支持它。我认为如果数组元素类型与指针类型相同,或者如果它iptr[size+1],这并不是一个严格的别名冲突。
(摘自GCC手册)
一个x86示例,其中字符串内存参数的长度未知。

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

如果可以避免在指针输入操作数上使用早期删除,则虚拟内存输入操作数通常会使用相同的寄存器选择简单的寻址模式。
但是,如果您使用早期的clobber来保证asm循环的严格正确性,那么有时一个伪操作数会使gcc在内存操作数的基址上浪费指令(和一个额外的寄存器)。检查编译器的asm输出。
背景:
这是内联asm示例中普遍存在的一个错误,该错误通常不会被发现,因为asm被包装在一个函数中,该函数不会内联到任何调用程序中,这些调用程序会诱使编译器重新排序存储,以便合并并消除死区存储。
gnu c内联asm语法是围绕向编译器描述单个指令而设计的。其目的是告诉编译器一个带有char"m"操作数约束的内存输入或内存输出,并选择寻址模式。
在内联asm中编写整个循环需要小心,以确保编译器真正知道发生了什么(或"=m"加上asm volatileblobber),否则在更改周围的代码或启用允许跨文件内联的链接时间优化时,可能会出现中断。
另请参见Looping over arrays with inline assembly使用"memory"语句作为循环体,仍在C中执行循环逻辑。对于实际(非伪)操作数asm"m"操作数,编译器可以通过在其选择的寻址模式中使用置换来展开循环。
脚注1:a"=m"clobber使编译器将asm视为非内联函数调用(可以读取或写入任何内存,但escape analysis已证明未转义的局部变量除外)。escape分析包括asm语句本身的输入操作数,也包括任何先前调用都可以存储指针的全局或静态变量。因此,通常本地循环计数器不必在"memory"语句周围溢出/重新加载asmblobber。
"memory"对于确保即使未使用其输出操作数也不会优化asm是必需的(因为您需要未声明的写入内存的副作用)。
或者对于仅由asm读取的内存,如果同一个输入缓冲区包含不同的输入数据,则需要asm再次运行。如果没有asm volatile,asm语句可能会CSEd超出循环。(当考虑是否需要运行volatile语句时,"memory"clobber不会使优化器将所有内存都当作输入。)
不带输出操作数的asm是隐式的asm,但最好将其显式化。(GCC手册有一节关于asm volatile)。
例如,volatile有一个输出操作数,因此不隐式易失性。如果你喜欢用它
 arr[5] = 1;
 total += asm_sum(arr, len);
 memcpy(arr, foo, len);
 total += asm_sum(arr, len);

如果没有asm("... sum an array ..." : "=r"(sum) : "r"(pointer), "r"(end_pointer) : "memory")第二个volatile可以进行优化,假设具有相同输入操作数(指针和长度)的相同asm将产生相同的输出。对于不是其显式输入操作数的纯函数的任何asm,都需要asm_sum。如果不进行优化,那么volatileblobber将具有要求内存同步的预期效果。

关于c - 如何指示可以使用内联ASM参数*指向*的内存?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31628554/

相关文章:

c++ - 使用VID,PID和SN查找USB端口号

c - 在没有最大缓冲区长度的情况下从 C 中的标准输入读取

c++ - 红帽 : using <atomic> compiles fine but linker can't find __atomic_store_16; what library?

c - libgcc_s.so : Error adding symbols: File in wrong format

c - 为什么这个函数在完成递归运行后返回整数 17?

c++ - 如何访问具有多个括号的一维数组以提高可读性?

linux - GCC 4.8 和 4.7 会在同一台机器上和平共存吗?

assembly - 无效操作数类型错误

从 Assembly 调用 C 函数——切换调用约定

c - 汇编零错误——switch语句