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

sil*_*sse 10 c assembly gcc linux-kernel

我正在阅读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;
}
Run Code Online (Sandbox Code Playgroud)

然后我调查了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
Run Code Online (Sandbox Code Playgroud)

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

asm("movq %%gs:%P1,%0" : "=r" (pfo_ret__) : "p"(&(kernel_stack))); 
Run Code Online (Sandbox Code Playgroud)

根据这篇文章,输入约束曾经是"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.
Run Code Online (Sandbox Code Playgroud)

同样在帖子 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().
Run Code Online (Sandbox Code Playgroud)

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

#include <stdio.h>

#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;
}
Run Code Online (Sandbox Code Playgroud)

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

Ros*_*dge 9

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

这意味着"p"约束的操作数必须具有有效的程序集表示,并且是有效地址.您正在尝试将堆栈上的变量地址用作操作数.虽然这是一个有效的地址,但它不是有效的操作数.堆栈变量本身具有有效的表示(类似于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已经在内核中进行了这种优化来替换一个"m"约束,"p"一般来说这样做一定是个好主意,你应该非常清楚这种优化是多么脆弱.它试图做的是愚弄GCC认为引用kernel_stack实际上并不访问内存,因此它不会在每次更改内存时继续重新加载值.这里的危险是如果kernel_stack确实发生了变化,那么编译器将被愚弄,并继续使用旧值.Linus知道每个CPU变量何时以及如何更改,因此可以确信宏在内核中用于其预期目的时是安全的.

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