在Linux内核中使用修饰符“P”和约束“p”超过“m”的gcc内联汇编

我正在阅读Linux内核源代码(3.12.5 x86_64)以了解如何处理进程描述符。

我发现获取当前进程描述符我可以使用current_thread_info()函数,其实现如下:

static inline struct thread_info *current_thread_info(void) { struct thread_info *ti; ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE); return ti; } 

然后我查看了this_cpu_read_stable()

 #define this_cpu_read_stable(var) percpu_from_op("mov", var, "p" (&(var))) #define percpu_from_op(op, var, constraint) \ ({ \ typeof(var) pfo_ret__; \ switch (sizeof(var)) { \ ... case 8: \ asm(op "q "__percpu_arg(1)",%0" \ : "=r" (pfo_ret__) \ : constraint); \ break; \ default: __bad_percpu_size(); \ } \ pfo_ret__; \ }) #define __percpu_arg(x) __percpu_prefix "%P" #x #ifdef CONFIG_SMP #define __percpu_prefix "%%"__stringify(__percpu_seg)":" #else #define __percpu_prefix "" #endif #ifdef CONFIG_X86_64 #define __percpu_seg gs #else #define __percpu_seg fs #endif 

扩展的宏应该是内联asm代码,如下所示:

 asm("movq %%gs:%P1,%0" : "=r" (pfo_ret__) : "p"(&(kernel_stack))); 

根据这篇文章 ,输入约束曾经是“m”(kernel_stack),这对我来说很有意义。 但显然提高性能Linus将约束更改为“p”并传递变量的地址:

 It uses a "p" (&var) constraint instead of a "m" (var) one, to make gcc think there is no actual "load" from memory. This obviously _only_ works for percpu variables that are stable within a thread, but 'current' and 'kernel_stack' should be that way. 

同样在post Tejun Heo发表了这样的评论:

 Added the magical undocumented "P" modifier to UP __percpu_arg() to force gcc to dereference the pointer value passed in via the "p" input constraint. Without this, percpu_read_stable() returns the address of the percpu variable. Also added comment explaining the difference between percpu_read() and percpu_read_stable(). 

但我的组合修饰符“P”修饰符和约束“p(&var)”的实验不起作用。 如果未指定段寄存器,则“%P1”始终返回变量的地址。 指针未被解除引用。 我必须使用括号来取消引用它,例如“(%P1)”。 如果指定了段寄存器,则不使用括号gcc甚至不编译。 我的测试代码如下:

 #include  #define current(var) ({\ typeof(var) pfo_ret__;\ asm(\ "movq %%es:%P1, %0\n"\ : "=r"(pfo_ret__)\ : "p" (&(var))\ );\ pfo_ret__;\ }) int main () { struct foo { int field1; int field2; } a = { .field1 = 100, .field2 = 200, }; struct foo *var = &a; printf ("field1: %d\n", current(var)->field1); printf ("field2: %d\n", current(var)->field2); return 0; } 

我的代码有什么问题吗? 或者我是否需要为gcc附加一些选项? 此外,当我使用gcc -S生成汇编代码时,我没有看到使用“p”超过“m”的优化。 任何答案或评论都非常感谢。

您的示例代码不起作用的原因是因为"p"约束仅在内联汇编中使用非常有限。 所有内联汇编操作数都要求它们可以表示为汇编语言中的操作数。 如果操作数不能代表编译器,那么首先将它移动到寄存器并将其替换为操作数。 "p"约束带来了额外的限制:操作数必须是有效地址。 问题是寄存器不是有效地址。 寄存器可以包含地址,但寄存器本身不是有效地址。

这意味着"p"约束的操作数必须具有有效的程序集表示,并且是有效地址。 您正在尝试将堆栈上的变量地址用作操作数。 虽然这是一个有效的地址,但它不是有效的操作数。 堆栈变量本身具有有效的表示forms(类似于8(%rbp) ),但堆栈变量的地址却没有。 (如果它是可表示的,它将类似于8 + %rbp ,但这不是合法的操作数。)

您可以将"p"约束的地址和用作操作数的少数事项之一是静态分配的变量。 在这种情况下,它是一个有效的程序集操作数,因为它可以表示为立即值(例如, &kernel_stack可以表示为$kernel_stack )。 它也是一个有效的地址,因此满足约束条件。

所以这就是为什么Linux内核宏工作而你的宏没有。 您正在尝试将其与堆栈变量一起使用,而内核仅将其与静态分配的变量一起使用。

或者至少看起来像编译器的静态分配变量。 事实上, kernel_stack实际上是在用于每个CPU数据的特殊部分中分配的。 此部分实际上不存在,而是用作模板为每个CPU创建单独的内存区域。 此特殊部分中kernel_stack的偏移量用作每个CPU数据区域中的偏移量,以便为每个CPU存储单独的内核堆栈值。 FS或GS段寄存器用作该区域的基础,每个CPU使用不同的地址作为基础。

这就是为什么Linux内核使用内联汇编来访问看起来像静态变量的东西。 宏用于将静态变量转换为每个CPU变量。 如果您不想尝试这样做,那么通过从内核宏复制可能无法获得任何好处。 您可能应该考虑采用不同的方式来完成您正在尝试完成的任务。

现在,如果您正在考虑,因为Linus Torvalds已经在内核中进行了这种优化以用"p"替换"m"约束,一般来说这是一个好主意,你应该非常清楚这个优化是多么脆弱。 它试图做的是愚弄GCC认为对kernel_stack引用实际上并不访问内存,因此每次更改内存时它都不会继续重新加载值。 这里的危险是如果kernel_stack确实发生了变化,那么编译器就会被愚弄,并继续使用旧值。 Linus知道每个CPU变量的更改时间和方式,因此可以确信宏在内核中用于其预期目的时是安全的。

如果您想在自己的代码中消除冗余负载,我建议使用-fstrict-aliasing和/或restrict关键字。 这样,您就不依赖于脆弱且不可移植的内联汇编宏。