2014-01-07 43 views
8

我正在閱讀Linux內核源代碼(3.12.5 x86_64)以瞭解如何處理進程描述符。在Linux內核中使用修飾符「P」和約束「p」在「m」上的gcc內聯彙編

我發現得到當前進程的描述符,我可以使用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("movq %%gs:%P1,%0" : "=r" (pfo_ret__) : "p"(&(kernel_stack))); 

根據this post,輸入約束用於「m」(kernel_stack),這對我很有意義。但顯然,以提高性能萊納斯改變了約束至「P」,並通過變量的地址:「

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(). 

但隨着合併修改我的實驗:

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許做這個評論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; 
} 

我的代碼有什麼問題嗎?或者我需要爲gcc添加一些選項?當我使用gcc -S生成彙編代碼時,我沒有看到通過在「m」上使用「p」來優化。任何答覆或評論非常感謝。

+1

詢問'gcc-help @ gcc.gnu.org'並給出編譯器的確切版本...... –

回答

8

您的示例代碼無法正常工作的原因是因爲"p"約束僅限於內聯彙編的非常有限的用途。所有內聯彙編操作數都要求它們可以用匯編語言表示爲操作數。如果操作數不可表示,那麼編譯器通過先將其移到一個寄存器並將其替換爲操作數來實現。約束條件限制了一個額外的限制:操作數必須是有效地址。問題是一個寄存器不是一個有效的地址。寄存器可以包含地址,但寄存器本身不是有效地址。

這意味着"p"約束的操作數必須具有有效的彙編表示,並且是有效的地址。你正試圖使用​​堆棧中變量的地址作爲操作數。雖然這是一個有效的地址,但它不是一個有效的操作數。堆棧變量本身有一個有效的表示(類似8(%rbp)),但堆棧變量的地址不是。 (如果它是可表示的,它將類似於8 + %rbp,但這不是合法的操作數。)

可以將地址和作爲操作數用於約束條件的少數幾件事之一是靜態分配變量。在這種情況下,它是一個有效的彙編操作數,因爲它可以表示爲一個立即值(例如,&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關鍵字。這樣你就不依賴於易碎和不可移植的內聯彙編宏。