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

Why is this pointer dereference ignored in this inline assembly statement?

在 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 语句中移出,我 do 会得到一个错误。

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'

任何人都可以阐明正在发生的事情吗?

Why doesn't the following dereference result in a memory access error?

因为您将它用作 asm 块的内存操作数,它不会直接取消引用,仅相对于 GS 段基数。 GS 基址设置为我们希望此线程的线程本地存储块所在的任何虚拟地址。

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


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

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

asm 模板替换是纯文本的。您可以执行 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 偏移量取消引用为指向平面虚拟地址 space 的指针(使用默认的 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 模板的操作数必须是指针取消引用,而不是本地操作数。启用优化后,可以优化局部并返回到原始位置的指针取消引用(如果使用使之成为可能的语义编写,与您的版本不同),但最好通过避免实际取消引用来确保它是安全的,而不是在 "m"(*ptr) 约束内的表达式中。