您當前的位置:首頁 > 收藏

CPU的執行級別和保護機制

作者:由 RobotCode俱樂部 發表于 收藏時間:2019-01-22

你可能已經知道在Intel x86計算機中,應用程式的能力是有限的,並且只有作業系統程式碼才能執行某些任務,但是你知道這是如何真正工作的嗎?本文將介紹x86特權級別(執行級別),即作業系統和CPU一起合作來限制使用者模式程式所能做的事情。有4個特權級別,編號為0(特權最大)到3(特權最小),以及

3個受保護的主要資源:記憶體、I/O埠和執行某些機器指令

的能力。在任何給定的時間,x86 CPU都以特定的特權級別執行,這決定了程式碼可以做什麼和不能做什麼。這些特權級別通常被描述為保護環,最裡面的環對應於最高的特權。大多數現代x86核心只使用兩個特權級別,0和3:

CPU的執行級別和保護機制

x86 Protection Rings

在數十條機器指令中,大約有15條被CPU限制為Ring 0指令。許多其他運算元都有限制。這些指令可能會破壞保護機制,或者在使用者模式允許的情況下引發混亂,因此它們保留給核心。如果試圖在Ring 0之外執行它們,就會導致通用保護異常,比如程式使用無效的記憶體地址。同樣,對記憶體和I/O埠的訪問也受到許可權級別的限制。但是在研究保護機制之前,我們先來看看CPU是如何跟蹤當前的特權級別的,這涉及到段選擇器,他們是:

CPU的執行級別和保護機制

Segment Selectors - Data and Code

資料段選擇器的全部內容由程式碼直接載入到各種段暫存器中,如ss(堆疊段暫存器)和ds(資料段暫存器)。這包括所請求的特權級別(RPL)欄位的內容,我們稍後將解釋它的含義。然而,程式碼段暫存器(cs)是不可思議的。首先,

它的內容不能由諸如mov這樣的載入指令直接設定,而只能由改變程式執行流(如call)的指令來設定。

其次,對我們來說很重要的一點是,與可以由程式碼設定的RPL欄位不同,cs具有由CPU本身維護的當前特權級別(CPL)欄位。程式碼段暫存器中的這個2位CPL欄位始終等於CPU的當前特權級別。英特爾文件在這一點上有些搖擺不定,有時線上文件會混淆這個問題,但這是一條硬性規定。在任何時候,無論CPU中發生了什麼,

檢視cs中的CPL都會告訴你正在執行的特權級別程式碼。

請記住,CPU特權級別與作業系統使用者無關。無論你是Root使用者、管理員、來賓還是普通使用者,都沒有關係。所有使用者程式碼都在環3中執行,所有核心程式碼都在環0中執行,而不管程式碼是代表哪個OS使用者執行的。有時某些核心任務可以被推到使用者模式,例如Windows Vista中的使用者模式裝置驅動程式,但這些只是為核心執行任務的特殊程序,通常可以在沒有重大後果的情況下終止。

由於對記憶體和I/O埠的訪問受到限制,使用者模式在不呼叫核心的情況下幾乎不能對外部世界做任何事情。

它不能開啟檔案、傳送網路資料包、列印到螢幕或分配記憶體。使用者程序執行在由Ring 0的眾神設定的嚴格限制的沙箱中。這就是為什麼從設計上來說,程序不可能洩漏其存在之外的記憶體或在其退出後保留開啟的檔案。所有控制這些東西的資料結構——記憶體、開啟的檔案等等——都不能被使用者程式碼直接訪問;一旦程序完成,沙箱就會被核心銷燬。這就是為什麼我們的伺服器可以有600天的正常執行時間——只要硬體和核心不出問題,就可以永遠執行。這也是為什麼Windows 95 / 98崩潰得如此之多:不是因為“M$ sucks”,而是因為重要的資料結構因為相容性的原因被使用者模式所訪問。儘管代價高昂,但在當時,這可能是一種不錯的權衡。

CPU在兩個關鍵時刻保護記憶體:載入段選擇器和使用線性地址訪問記憶體頁。

因此,在涉及分段和分頁的情況下,保護對映記憶體地址轉換。當載入資料段選擇器時,進行如下檢查:

CPU的執行級別和保護機制

x86 Segment Protection

由於更高的數字意味著更少的特權,上面的MAX()選擇CPL和RPL中特權最少的,並將其與描述符特權級別(DPL)進行比較。如果DPL更高或相等,則允許訪問。RPL背後的思想是允許核心程式碼使用降低的特權載入段。例如,你可以使用RPL(3)來確保給定的操作使用使用者模式可訪問的段。堆疊段暫存器ss是例外,CPL、RPL和DPL這三個暫存器必須完全匹配。

實際上,段保護並不重要,因為現代核心使用扁平的地址空間,其中使用者模式的段可以到達整個線性地址空間。在分頁單元中,當線性地址轉換為物理地址時,可以進行有用的記憶體保護。每個記憶體頁是一個位元組塊,由頁表項描述,其中包含兩個與保護相關的欄位:一個監控器標誌和一個讀/寫標誌。supervisor標誌是核心使用的主要x86記憶體保護機制。開啟時,無法從環3訪問該頁。雖然讀/寫標誌對於強制特權來說不那麼重要,但它仍然很有用。當載入程序時,儲存二進位制(程式碼)的頁面被標記為只讀,因此,如果程式試圖寫入這些頁面,就會捕獲一些指標錯誤。此標誌還用於在Unix中生成子程序時實現寫時複製。在Fork時,父級頁面被標記為只讀,並與Fork後的子級共享。如果任何一個程序試圖寫入頁面,處理器就會觸發一個錯誤,核心就會知道複製此頁面,並將其標記為可以讀/寫。

最後,我們需要一種讓CPU在特權級別之間切換的方法。如果環3程式碼可以將控制轉移到核心中的任意位置,那麼很容易透過跳到錯誤的位置來破壞作業系統。有必要進行受控的轉移。這是透過門描述符和sysenter指令實現的。門描述符是型別系統的段描述符,有四種子型別:呼叫門描述符、中斷門描述符、陷阱門描述符和任務門描述符。呼叫門提供了一個核心入口點,可以與普通呼叫和jmp指令一起使用,但是它們的使用並不多,因此我將忽略它們。任務門也不是那麼熱門(在Linux中,它們只在由核心或硬體問題引起的雙故障中使用)。

這樣就剩下兩個更有趣的:中斷和陷阱門,它們用於處理硬體中斷(如鍵盤、計時器、磁碟)和異常(如頁面錯誤,除0)。我將兩者都稱為“中斷”。這些門描述符儲存在中斷描述符表(IDT)中。每個中斷被分配一個介於0和255之間的數字,稱為vector,處理器在確定處理中斷時使用哪個門描述符時,將其作為IDT的索引。中斷門和陷阱門幾乎是相同的。它們的格式如下所示,以及在發生中斷時強制執行的特權檢查。我為Linux核心填充了一些值,以使事情具體化。

CPU的執行級別和保護機制

Interrupt Descriptor with Privilege Check

門中的DPL和段選擇器都控制訪問,而段選擇器加上偏移量一起為中斷處理程式程式碼確定了一個入口點。在這些門描述符中,核心通常使用段選擇器來選擇核心程式碼段。中斷永遠不能將控制權從特權更大的環轉移到特權更小的環。特權必須保持不變(當核心本身被中斷時)或提升(當用戶模式程式碼被中斷時)。無論哪種情況,得到的CPL都等於目的碼段的DPL;如果CPL發生變化,還會發生堆疊切換。如果一箇中斷是由程式碼透過int n這樣的指令觸發的,那麼還要進行一次檢查:gate DPL必須具有與CPL相同或更低的特權。這可以防止使用者程式碼觸發隨機中斷。如果這些檢查失敗,就會發生一般保護異常。

所有Linux中斷處理程式最終都在ring 0中執行

在初始化期間,Linux核心首先在setup_idt()中設定一個忽略所有中斷的IDT。然後使用include/asm-x86/desc。h中的函式來充實arch/x86/kernel/traps_32。c中常見的IDT條目。在Linux中,名稱中帶有“system”的gate描述符可以從使用者模式訪問,其set函式使用的DPL為3。“系統門”是一種可以進入使用者模式的英特爾陷阱門。但是,這裡沒有設定硬體中斷門,而是在適當的驅動程式中設定。

使用者模式可以訪問三個門:向量3和4分別用於除錯和檢查數值溢位。然後為SYSCALL_VECTOR設定一個系統門,對於x86體系結構是0x80。這是一種機制,用於程序將控制權轉移到核心,進行系統呼叫,在我申請“int 0x80”虛榮車牌的時候:)。從奔騰Pro開始,sysenter指令作為一種更快的系統呼叫方式被引入。它依賴於專門用於儲存核心系統呼叫處理程式的程式碼段、入口點和其他的CPU暫存器。當sysenter執行時,CPU不進行特權檢查,立即進入CPL 0,並將新值載入到暫存器中,用於程式碼和堆疊(cs、eip、ss和esp)。只有Ring 0可以載入sysenter設定暫存器,這是在enable_sep_cpu()中完成的。

最後,當返回到環3時,核心發出iret或sysexit指令,分別從中斷和系統呼叫返回,從而離開環0和恢復執行CPL為3的使用者程式碼。我們的x86環和保護之旅到此結束。感謝你的閱讀!

——未完待續

由於本人水平有限,翻譯必然有很多不妥的地方,歡迎指正。

同時,歡迎關注下方微信公眾號,一起交流學習:)

CPU的執行級別和保護機制

標簽: 核心  特權  CPU  中斷  描述符