Gcc 为什么在此内联汇编语句中忽略此指针取消引用?

Gcc 为什么在此内联汇编语句中忽略此指针取消引用?,gcc,x86,inline-assembly,llvm-clang,xnu,Gcc,X86,Inline Assembly,Llvm Clang,Xnu,在XNU源代码中,特别是有一个用于快速访问线程本地数据的功能: __attribute__((always_inline)) static __inline__ void* _os_tsd_get_direct(unsigned long slot) { void *ret; __asm__("mov %%gs:%1, %0" : "=r" (ret) : "m" (*(void **)(slot * sizeof(void *)))); return ret; } 我

在XNU源代码中,特别是
有一个用于快速访问线程本地数据的功能:

__attribute__((always_inline))
static __inline__ void*
_os_tsd_get_direct(unsigned long slot)
{
    void *ret;
    __asm__("mov %%gs:%1, %0" : "=r" (ret) : "m" (*(void **)(slot * sizeof(void *))));
    return ret;
}
我对编译器解释内联程序集的方式感到困惑

假设
slot==1
。在x86_64上,sizeof(void*)==8,因此输入操作数表达式变为
*(void**)(8)
。为什么以下取消引用不会导致内存访问错误

事实上,如果我试图将表达式移出
asm
语句,我确实会得到一个错误

void * my_os_tsd_get_direct(unsigned long slot) {
    void *ret;
    void *ptr = *(void **)(slot * sizeof(void *));
    __asm__("mov %%gs:%1, %0" : "=r" (ret) : "m" (ptr));
    return ret;
}
我查看了汇编程序的输出,发现第二个版本取消了对指针的引用,但第一个版本没有

所以我想,好吧,让我们尝试删除
asm
语句中的显式解引用,因为编译器似乎忽略了它

void * my_os_tsd_get_direct_v2(unsigned long slot) {
    void *ret;
    __asm__("mov %%gs:%1, %0" : "=r" (ret) : "m" ((void *)(slot * sizeof(void *))));
    return ret;
}
但这会产生
错误:asm输入中约束“m”的左值无效

有人能解释一下发生了什么事吗

为什么以下取消引用不会导致内存访问错误

因为您将它用作asm块的内存操作数,asm块不会直接对其进行反求,只会相对于GS段基GS base设置为我们希望该线程的线程本地存储块所在的任何虚拟地址。

有关Linux上的gcc如何使用FS或GS段寄存器实现线程本地存储(TLS),请参阅和/或。XNU显然在做基本相同的事情,但是使用内联asm而不是利用GNUC内置线程


“m”
约束有点类似于C的
&
运算符:编译器不将对象加载到寄存器中,而只是替换将对象引用到asm模板中的寻址模式

由于此asm模板不直接使用寻址模式,而是使用
%%gs:
,因此它实际上并没有取消对
*(void**)(slot*sizeof(void*))
的引用,如果将其分配给纯C中的变量,则会发生这种情况

asm模板替换是纯文本的。您可以执行类似于
16+%0
的操作来访问内存操作数前面16字节的内存位置


通常,查看编译器的asm输出会有所帮助。我把你的代码,并删除了静态内联的东西,所以我们可以看到一个独立的函数定义asm

void*
_os_tsd_get_direct(unsigned long slot)
{
    void *ret;
    __asm__("mov %%gs:%1, %0\n\t"
            "nop  # operand 1 was %1" : "=r" (ret) : "m" (*(void **)(slot * sizeof(void *))));
    return ret;
}
集合到

#gcc -O3
    mov %gs:0(,%rdi,8), %rax
    nop                       # operand 1 was 0(,%rdi,8)
    ret
我使用了NOP而不仅仅是注释,所以即使Godbolt删除了仅注释行,它仍然可见。添加显示模板操作数是什么的伪注释通常很方便(特别是当您使用任何隐式操作数指令,并且希望看到编译器为模板中未提及的操作数选择了什么时)

在这里,我添加它只是为了说明编译器所替代的
0(,%rdi,8)
只是文本,可以在您要求的任何地方使用。诀窍是,我们在一个
%%gs:
之后立即请求它


void*ptr=*(void**)(插槽*sizeof(void*)

那是在做完全不同的事情。实际上,您正在将TLS偏移作为指针解引用到平面虚拟地址空间(使用默认的DS段基=0)

如果你想分手,你会的

void * separated_os_tsd_get_direct(unsigned long slot) {
    void *ret;
    unsigned long slot_offset = slot * sizeof(void*);
    void **gs_ptr = (void **)slot_offset;
    __asm__("mov %%gs:%1, %0" : "=r" (ret) : "m" (*gs_ptr));
    return ret;
}
汇编至:

separated_os_tsd_get_direct(unsigned long):
    mov %gs:0(,%rdi,8), %rax
    ret
asm模板的操作数必须是指针解引用,而不是本地操作数。启用优化后,可以对局部进行优化,并将其转换回原始位置的指针deref(如果使用语义编写,则与您的版本不同),但最好避免在
“m”(*ptr)
约束中的表达式之外的实际deref,以确保其安全

为什么以下取消引用不会导致内存访问错误

因为您将它用作asm块的内存操作数,asm块不会直接对其进行反求,只会相对于GS段基GS base设置为我们希望该线程的线程本地存储块所在的任何虚拟地址。

有关Linux上的gcc如何使用FS或GS段寄存器实现线程本地存储(TLS),请参阅和/或。XNU显然在做基本相同的事情,但是使用内联asm而不是利用GNUC内置线程


“m”
约束有点类似于C的
&
运算符:编译器不将对象加载到寄存器中,而只是替换将对象引用到asm模板中的寻址模式

由于此asm模板不直接使用寻址模式,而是使用
%%gs:
,因此它实际上并没有取消对
*(void**)(slot*sizeof(void*))
的引用,如果将其分配给纯C中的变量,则会发生这种情况

asm模板替换是纯文本的。您可以执行类似于
16+%0
的操作来访问内存操作数前面16字节的内存位置


通常,查看编译器的asm输出会有所帮助。我把你的代码,并删除了静态内联的东西,所以我们可以看到一个独立的函数定义asm

void*
_os_tsd_get_direct(unsigned long slot)
{
    void *ret;
    __asm__("mov %%gs:%1, %0\n\t"
            "nop  # operand 1 was %1" : "=r" (ret) : "m" (*(void **)(slot * sizeof(void *))));
    return ret;
}
集合到

#gcc -O3
    mov %gs:0(,%rdi,8), %rax
    nop                       # operand 1 was 0(,%rdi,8)
    ret
我使用了NOP而不仅仅是注释,所以即使Godbolt删除了仅注释行,它仍然可见。添加显示模板操作数是什么的伪注释通常很方便(特别是当您使用任何隐式操作数指令,并且希望看到编译器为模板中未提及的操作数选择了什么时)

在这里,我添加它只是为了说明编译器所替代的
0(,%rdi,8)
只是文本,可以在您要求的任何地方使用。诀窍是,我们在一个
%%gs:
之后立即请求它


void*ptr=*(void**)(插槽*sizeof(