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

时间:2018-10-22 19:41:41

标签: gcc x86 inline-assembly llvm-clang xnu

在XNU源文件中,特别是<libsyscall/os/tsd.h>中,有一个用于快速访问线程本地数据的功能:

__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;
}

但是那个产生error: invalid lvalue in asm input for constraint 'm'

任何人都可以对正在发生的事情有所了解吗?

1 个答案:

答案 0 :(得分:3)

  

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

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

有关Linux上的gcc如何使用FS或GS段寄存器实现线程本地存储(TLS)的信息,请参见How does the gcc `__thread` work?和/或Addresses of Thread Local Storage Variables。 XNU显然在做基本上相同的事情,但是使用内联asm而不是利用GNU C内置函数来处理线程。


"m"约束有点类似于C的&运算符:编译器没有将对象加载到寄存器中,而是仅将引用对象的地址模式替换为asm模板。

由于此asm模板不直接使用寻址模式,而是使用%%gs:,因此实际上并没有取消对*(void **)(slot * sizeof(void *)))的引用,如果您分配了到纯C中的变量。

asm-template替换纯粹是文本的。您可以执行16 + %0之类的操作来访问比存储操作数高16个字节的存储位置。


与往常一样,它有助于查看编译器的asm输出。我放置了您的代码on the Godbolt compiler explorer (with gcc and clang),并删除了静态内联控件,以便我们可以看到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 **)(slot * sizeof(void *));

这是完全不同的事情。您实际上在将TLS偏移作为指向平面虚拟地址空间的指针而取消引用(使用默认的DS段base = 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模板的操作数必须是指针取消引用,而不是局部引用。启用优化功能后,可以优化本地变量,然后将其转换回原始位置的指针反引用(如果使用使之成为可能的语义编写,与您的版本不同),但最好避免使用实际的反引用,以确保其安全性,在"m"(*ptr)约束内的表达式中。