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

C 如何指示可以使用内联ASM参数*指向*的内存?,c,gcc,assembly,clang,inline-assembly,C,Gcc,Assembly,Clang,Inline Assembly,考虑以下小功能: void foo(int* iptr) { iptr[10] = 1; __asm__ volatile ("nop"::"r"(iptr):); iptr[10] = 2; } void foo2(int* iptr, long* lptr) { iptr[10] = 1; lptr[20] = 100; __asm__ volatile ("nop"::"r"(iptr):); iptr[10] = 2; l

考虑以下小功能:

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

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

我可以告诉编译器,内存必须是最新的
memory
clobber,如下所示:

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 foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
}
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”
clobber无法实现这一点,因为它意味着两次写入都必须发生:

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和memory

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

虚拟内存输入:
“m”(*(常量int(*)[])iptr)

或输出:
“=m”(*(int(*)[])iptr)
。当然,也可以使用相同语法的
“+m”

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

如果使用
[]
未指定大小,则会告诉GCC相对于该指针访问的任何内存都是输入、输出或输入/输出操作数。
如果使用
[10]
[some_variable]
,则会告诉编译器具体大小。对于运行时变量大小,gcc实际上忽略了优化,
iptr[size+1]
不是输入的一部分

因此支持它。我认为,如果数组元素类型与指针相同,或者如果数组元素类型是
char
,则不会出现严格的别名冲突

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

   asm("repne scasb"
    : "=c" (count), "+D" (p)
    : "m" (*(const char (*)[]) p), "0" (-1), "a" (0));
如果可以避免在指针输入操作数上使用早期重击器,则虚拟内存输入操作数通常会使用同一寄存器选择简单寻址模式

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


背景: 在内联asm示例中,这是一个普遍存在的错误,通常未被检测到,因为asm封装在一个函数中,而该函数不内联到任何调用方中,从而诱使编译器重新排序存储以进行合并,从而消除死区存储

GNUCinlineASM语法是围绕向编译器描述一条指令而设计的。其目的是告诉编译器关于带有
“m”
“=m”
操作数约束的内存输入或内存输出,并且编译器选择寻址模式

在内联asm中编写整个循环需要小心,以确保编译器真正知道发生了什么(或
asm volatile
加上
内存“
clobber),否则在更改周围代码或启用允许跨文件内联的链接时间优化时,可能会出现中断的风险

另请参见使用
asm
语句作为循环体,仍然使用C语言执行循环逻辑。对于实际(非伪)
“m”
“=m”
操作数,编译器可以在其选择的寻址模式中使用置换来展开循环


脚注1:clobber使编译器将asm视为非内联函数调用(可以读取或写入任何内存,但已证明未转义的局部变量除外)。转义分析包括asm语句本身的输入操作数,还包括任何早期调用可能已将指针存储到其中的任何全局或静态变量。因此,通常本地循环计数器不必在带有
“memory”
clobber的
asm
语句周围溢出/重新加载

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

或者,对于仅由asm读取的内存,如果同一输入缓冲区包含不同的输入数据,则需要再次运行asm。如果没有
volatile
,asm语句可能会跳出循环。(在考虑是否需要运行
asm
语句时,
“内存”
clobber不会使优化器将所有内存视为输入。)

没有输出操作数的
asm
是隐式的
volatile
,但最好是显式的。(海合会手册)