在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'
。
任何人都可以对正在发生的事情有所了解吗?
答案 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)
约束内的表达式中。