2013-02-22 55 views
7

linux/arch/x86/include/asm/switch_to.h,有宏觀switch_to的定義,做閱讀這樣的真正的線程切換奇蹟的關鍵線路(直到版Linux 4.7時,它改變):爲什麼switch_to使用push + jmp + ret來改變EIP,而不是直接改變jmp?

asm volatile("pushfl\n\t"  /* save flags */ \ 
       pushl %%ebp\n\t"  /* save EBP */ \ 
       "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ 
       "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ 
       "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ 
       "pushl %[next_ip]\n\t" /* restore EIP */ \ 
       __switch_canary     \ 
       "jmp __switch_to\n" /* regparm call */ \ 
       "1:\t"      \ 
       "popl %%ebp\n\t"  /* restore EBP */ \ 
       "popfl\n"   /* restore flags */ \ 

命名的操作數有像[prev_sp] "=m" (prev->thread.sp)內存限制。除非定義了CONFIG_CC_STACKPROTECTOR(然後它是使用%ebx的加載和存儲),否則定義爲__switch_canary

我理解它是如何工作的,如內核堆棧指針備份/恢復,以及如何push next->eipjmp __switch_to在函數的結束,這實際上是一個真正的ret相匹配的「假」 CALL指令ret指令指令,並有效地使下一個線程的返回點成爲next->eip

我不明白的是,爲什麼黑客?爲什麼不只是call __switch_to,之後它ret,jmpnext->eip,這是更乾淨和易於閱讀。

回答

5

這樣做有兩個原因。

其中之一是允許操作數/寄存器分配的完全靈活性爲[next_ip]。如果你希望能夠做jmp %[next_ip]call __switch_to那麼就必須有%[next_ip]分配到非易失性寄存器(即一個由ABI定義,將使得函數調用時保留其值 )。

這引入了編譯器優化能力的限制,並且context_switch()('調用者' - 其中switch_to()被使用)的結果代碼可能不如可能。但是爲了什麼好處呢?

好 - 這就是第二個原因來自於,沒有,真的,因爲call __switch_to將相當於:

pushl 1f 
jmp __switch_to 
1: jmp %[next_ip] 

即其將返回地址;你最終得到一個序列push/jmp== call)/ ret/jmp而如果你不想返回到這個地方(而且這段代碼沒有),你可以通過「僞裝」一個調用來保存代碼分支,因爲你只需要做push/jmp/ret。代碼自己尾遞歸在這裏。

是的,這是一個小的優化,但避免分支減少延遲和延遲對上下文切換至關重要。

+2

但它不會有效殺死回報預測堆棧嗎? – harold 2013-02-22 12:37:20

+0

是的 - 但是註冊目標的'jmp'也是這樣做的,因爲你_never_想真的返回'switch_to()',真的(直到下一個上下文切換)。兩者之間沒有區別。 – 2013-02-22 13:20:11

+0

@harold:好點; (''in'kernel/sched/core.c'](http://elixir.free-electrons.com)返回的上下文切換之後,返回地址預測器棧的當前內容是有價值的/linux/v4.6/source/kernel/sched/core.c#L2752)),並且可能有幾個級別將調用棧備份到調度程序中(直到它們的後退分歧)。但是,只有'%[next_ip]'總是/通常在switch_to內時纔是真的;它確實以這種方式設置了'prev_ip',但也許這不是最常見的值(內核搶佔可能會將其留在別的地方?) – 2017-12-09 21:31:24