歡迎光臨
每天分享高質量文章

從貓蛇之戰三看內核戲CPU

    廬山歸來,終於有些空閑,見縫插針,今天趕緊把沒有寫完的“貓蛇之戰”補齊。

    如果沒有讀過前兩篇或者想複習一下的,請點擊:

從貓蛇之戰看內核戲CPU

從貓蛇之戰再看內核戲CPU

先說明一下,“連續劇”的成本有點高,無論如何,這一篇會把這個問題寫完。

回顧一下,最初的問題是“為什麼在除錯器里讀寫空指標不會崩潰?” 第一篇通過讀原始碼的方法揭示了除錯器會使用特殊的probe函式:

probe_kernel_read

probe_kernel_write

上一篇通過試驗證實,使用probe函式時CPU也會發怒報異常。本篇繼續介紹CPU報了異常之後,內核是如何處理這個事件,將其“擺平”的。

在著名的《幽夢影》一書中有很多妙語,其中有不少是關於寫作技巧的,比如:

“作文之法: 意之曲折者,宜寫之以顯淺之詞; 理之顯淺者,宜運之以曲折之筆; ”

因為這個系列討論的問題有點複雜和曲折,所以我們是遵循“意之曲折者,宜寫之以顯淺之詞”的原則來寫的。

繼續貫徹這個原則,直接回答剛纔的問題,“擺平”CPU靠的是LINUX內核里一種基於表的異常處理機制,這個機制一般被稱為“異常表(Exception Table)”,簡稱extable。

下麵繼續結合我們故意訪問地址880的例子來理解extable機制。

在CPU查找頁表發現線性地址0x880無效而發怒後,它通過IDT表中登記地址跳轉到LINUX中處理異常的入口函式,這個入口函式是以彙編語言編寫的,名為page_fault,在arch/x86/entry/entry_64.S中。

彙編函式不適合做太多邏輯,只是儲存暫存器等信息後便呼叫C語言編寫的do_page_fault。

do_page_fault內部獲取CR2的值後便呼叫__do_page_fault。

__do_page_fault內部的邏輯錯綜複雜,一個條件判斷接著另一個,我們只挑與我們有關的說。

與try{}catch等異常捕捉機制類似,extable機制也是需要編譯期就做好準備的。

仔細觀察probe函式所呼叫的拷貝函式,可以看到在它的末尾是有些特別機關的。

    註意上圖中的兩個_ASM_EXTABLE宏,它們就是給危險代碼增加保險(異常處理)的“安全帶”。

這個宏定義在asm.h,如下圖所示。

    閱讀上面的宏,其作用是在專門描述異常處理器的異常表(__extable)里增加一行,這一行包含三個信息:

from

to

handler

簡單來說,前兩個都是代碼地址,一個是觸發異常的,一個是處理異常的,最後一個是函式指標。最後一個是4.6版本內核新增的,為了支持更複雜的處理策略。在_ASM_EXTABLE宏中,使用的是ex_handler_default,選擇這個的處理器的效果是:如果from處發生異常,那麼就跳轉到to處執行,不要panic,也不要發信號,封鎖信息,低調處理,像什麼都發生一樣。

異常表表項的結構體定義在extable.h中,即:

    struct exception_table_entry {

    int insn, fixup, handler;

    };

    在extable.c檔案中,有ex_handler_default函式的代碼,摘錄如下:

    __visible bool ex_handler_default(

                const struct exception_table_entry *fixup,

         struct pt_regs *regs, int trapnr)

    {

     regs->ip = ex_fixup_addr(fixup);

     return true;

    }

    EXPORT_SYMBOL(ex_handler_default);

    各位看官請睜大眼睛,到關鍵地方了。請特別註意加粗的那一行代碼,左邊寫的是regs結構體中的程式指標(ip),右邊是處理異常代碼的位置(即to引數)。

進一步說,這個regs結構體是在棧上形成的,報告異常時,CPU在準備起飛前先壓入當時的執行位置,也就是段暫存器和程式指標,跳到page_fault後,內核中的代碼繼續把其它暫存器也壓入棧,於是就在棧上形成了一個資料結構。對於熟悉NT內核的朋友來說,這相當於那個著名的陷阱幀(TRAP_FRAME)。

這種直接修改程式指標的方法是內核處理危機的殺手鐧。經過這樣飛針後,__do_page_fault就直接傳回了,do_page_fault也傳回,到了彙編寫的page_fault函式後,就開始恢復暫存器了,也就是把儲存在棧上的regs結構體中的暫存器彈出棧,加載到CPU中的物理暫存器。

軟體儲存的暫存器都恢復好後,執行iret指令。

    執行iret指令時,CPU從棧上彈出已經被修改了的ip暫存器,跳過去執行。於是便開始執行to指定的異常處理代碼了。這個代碼在Linux內核中,被稱為fixup,意思是“修修補補”。下圖記錄了這個特別飛躍的過程。

    上面是CPU執行iret前的棧內容,最上面便是IP和CS。單步一下後,CPU執行iret,從棧上彈出CS:IP,跳轉到修補代碼。

好一個飛躍,這一躍,從隨時可能跌入深淵的do_page_fault中跳出,告別了敏感的異常處理背景關係,化險為夷了。

這一跳躍,很像是貓蛇之戰時小貓的緊急後退。小貓伸爪挑逗毒蛇是為了消耗蛇的體力,被激怒的毒蛇舉頭襲擊,很是危險,小貓巧妙躲閃,靈活後退,華麗轉身。

    在原始碼中,修補函式是有特別標註的,放在特殊的.fixup段中,比如:

.section .fixup,”ax”

.L_fixup_4x8b_copy:

shll $6,%ecx

addl %ecx,%edx

jmp .L_fixup_handle_tail

執行好修補代碼片段後,因為儲存在棧上的copy函式的傳回地址並沒有變化,所以當修補函式傳回時,執行緒會傳回到probe函式中繼續執行。並且,從probe函式看來,copy函式的傳回值不為0,代表剩下的位元組數,正常copy時,copy函式傳回前會將ax暫存器置零,代表完成所有複製任務。因此,probe函式便可以根據copy函式的傳回值不為0而傳回-EFAULT了,也就是我們在第一篇文章中曾經解釋過的這個代碼。

    講到這裡,第三個問題(Q3)的答案也有了。那麼第二個問題呢?如果充分理解了上面描述的過程,那麼也可以回答了,留著給大家思考吧。

最後分享一張老雷在廬山所拍的照片吧。

    上了很多次廬山,第一次遇上山上的白玉蘭盛開,高大的樹木上掛滿花朵,遠遠就可以望見。走到近處,花香襲人,坐在石階上,透過鮮花和樹幹,還可以欣賞不遠處的瀑布,來自廬山主峰漢陽峰的一股清泉,奔流直下,拍擊岩石,濺出水珠無數……

    赞(0)

    分享創造快樂