1. 程式人生 > >CPU Rings, Privilege, and Protection.CPU的運行環, 特權級與保護

CPU Rings, Privilege, and Protection.CPU的運行環, 特權級與保護

內存地址 能力 只讀 調用門 request bsp 管理員 vector sel

原文標題:CPU Rings, Privilege, and Protection

原文地址:http://duartes.org/gustavo/blog/

[註:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]

可能你憑借直覺就知道應用程序的功能受到了Intel x86計算機的某種限制,有些特定的任務只有操作系統的代碼才可以完成,但是你知道這到底是怎麽一回事嗎?在這篇文章裏,我們會接觸到x86的特權級(privilege level),看看操作系統和CPU是怎麽一起合謀來限制用戶模式的應用程序的。特權級總共有4個,編號從0(最高特權)到3(最低特權)。有3種主要的資源受到保護:內存,I/O端口以及執行特殊機器指令的能力。在任一時刻,x86 CPU都是在一個特定的特權級下運行的,從而決定了代碼可以做什麽,不可以做什麽。這些特權級經常被描述為保護環(protection ring),最內的環對應於最高特權。即使是最新的x86內核也只用到其中的2個特權級:0和3。

技術分享圖片

x86的保護環

在諸多機器指令中,只有大約15條指令被CPU限制只能在ring 0執行(其余那麽多指令的操作數都受到一定的限制)。這些指令如果被用戶模式的程序所使用,就會顛覆保護機制或引起混亂,所以它們被保留給內核使用。如果企圖在ring 0以外運行這些指令,就會導致一個一般保護錯(general-protection exception),就像一個程序使用了非法的內存地址一樣。類似的,對內存和I/O端口的訪問也受特權級的限制。但是,在我們分析保護機制之前,先讓我們看看CPU是怎麽記錄當前特權級的吧,這與前篇文章中提到的段選擇符(segment selector)有關。如下所示:

技術分享圖片

數據段和代碼段的段選擇符

數據段選擇符的整個內容可由程序直接加載到各個段寄存器當中,比如ss(堆棧段寄存器)和ds(數據段寄存器)。這些內容裏包含了請求特權級(Requested Privilege Level,簡稱RPL)字段,其含義過會兒再說。然而,代碼段寄存器(cs)就比較特別了。首先,它的內容不能由裝載指令(如MOV)直接設置,而只能被那些會改變程序執行順序的指令(如CALL)間接的設置。而且,不像那個可以被代碼設置的RPL字段,cs擁有一個由CPU自己維護的當前特權級字段(Current Privilege Level,簡稱CPL),這點對我們來說非常重要。這個代碼段寄存器中的2位寬的CPL字段的值總是等於

CPU的當前特權級。Intel的文檔並未明確指出此事實,而且有時在線文檔也對此含糊其辭,但這的確是個硬性規定。在任何時候,不管CPU內部正在發生什麽,只要看一眼cs中的CPL,你就可以知道此刻的特權級了。

記住,CPU特權級並不會對操作系統的用戶造成什麽影響,不管你是根用戶,管理員,訪客還是一般用戶。所有的用戶代碼都在ring 3上執行,所有的內核代碼都在ring 0上執行,跟是以哪個OS用戶的身份執行無關。有時一些內核任務可以被放到用戶模式中執行,比如Windows Vista上的用戶模式驅動程序,但是它們只是替內核執行任務的特殊進程而已,而且往往可以被直接刪除而不會引起嚴重後果。

由於限制了對內存和I/O端口的訪問,用戶模式代碼在不調用系統內核的情況下,幾乎不能與外部世界交互。它不能打開文件,發送網絡數據包,向屏幕打印信息或分配內存。用戶模式進程的執行被嚴格限制在一個由ring 0之 神所設定的沙盤之中。這就是為什麽從設計上就決定了:一個進程所泄漏的內存會在進程結束後被統統回收,之前打開的文件也會被自動關閉。所有的控制著內存或 打開的文件等的數據結構全都不能被用戶代碼直接使用;一旦進程結束了,這個沙盤就會被內核拆毀。這就是為什麽我們的服務器只要硬件和內核不出毛病,就可以 連續正常運行600天,甚至一直運行下去。這也解釋了為什麽Windows 95/98那麽容易死機:這並非因為微軟差勁,而是因為系統中的一些重要數據結構,出於兼容的目的被設計成可以由用戶直接訪問了。這在當時可能是一個很好的折中,當然代價也很大。

CPU會在兩個關鍵點上保護內存:當一個段選擇符被加載時,以及,當通過線形地址訪問一個內存頁時。因此,保護也反映在內存地址轉換的過程之中,既包括分段又包括分頁。當一個數據段選擇符被加載時,就會發生下述的檢測過程:

技術分享圖片

x86的分段保護

因為越高的數值代表越低的特權,上圖中的MAX()用於挑出CPL和RPL中特權最低的一個,並與描述符特權級(descriptor privilege level,簡稱DPL)比較。如果DPL的值大於等於它,那麽這個訪問就獲得許可了。RPL背後的設計思想是:允許內核代碼加載特權較低的段。比如,你可以使用RPL=3的段描述符來確保給定的操作所使用的段可以在用戶模式中訪問。但堆棧段寄存器是個例外,它要求CPL,RPL和DPL這3個值必須完全一致,才可以被加載。

事實上,段保護功能幾乎沒什麽用,因為現代的內核使用扁平的地址空間。在那裏,用戶模式的段可以訪問整個線形地址空間。真正有用的內存保護發生在分頁單元中,即從線形地址轉化為物理地址的時候。一個內存頁就是由一個頁表項(page table entry)所描述的字節塊。頁表項包含兩個與保護有關的字段:一個超級用戶標誌(supervisor flag),一個讀寫標誌(read/write flag)。超級用戶標誌是內核所使用的重要的x86內存保護機制。當它開啟時,內存頁就不能被ring 3訪問了。盡管讀寫標誌對於實施特權控制並不像前者那麽重要,但它依然十分有用。當一個進程被加載後,那些存儲了二進制鏡像(即代碼)的內存頁就被標記為只讀了,從而可以捕獲一些指針錯誤,比如程序企圖通過此指針來寫這些內存頁。這個標誌還被用於在調用fork創建Unix子進程時,實現寫時拷貝功能(copy on write)。

最後,我們需要一種方式來讓CPU切換它的特權級。如果ring 3的程序可以隨意的將控制轉移到(即跳轉到)內核的任意位置,那麽一個錯誤的跳轉就會輕易的把操作系統毀掉了。但控制的轉移是必須的。這項工作是通過門描述符(gate descriptor)和sysenter指令來完成的。一個門描述符就是一個系統類型的段描述符,分為了4個子類型:調用門描述符(call-gate descriptor),中斷門描述符(interrupt-gate descriptor),陷阱門描述符(trap-gate descriptor)和任務門描述符(task-gate descriptor)。調用門提供了一個可以用於通常的CALL和JMP指令的內核入口點,但是由於調用門用得不多,我就忽略不提了。任務門也不怎麽熱門(在Linux上,它們只在處理內核或硬件問題引起的雙重故障時才被用到)。

剩下兩個有趣的:中斷門和陷阱門,它們用來處理硬件中斷(如鍵盤,計時器,磁盤)和異常(如缺頁異常,0除數異常)。我將不再區分中斷和異常,在文中統一用"中斷"一詞表示。這些門描述符被存儲在中斷描述符表(Interrupt Descriptor Table,簡稱IDT)當中。每一個中斷都被賦予一個從0到255的編號,叫做中斷向量。處理器把中斷向量作為IDT表項的索引,用來指出當中斷發生時使用哪一個門描述符來處理中斷。中斷門和陷阱門幾乎是一樣的。下圖給出了它們的格式。以及當中斷發生時實施特權檢查的過程。我在其中填入了一些Linux內核的典型數值,以便讓事情更加清晰具體。

技術分享圖片

伴隨特權檢查的中斷描述符

門中的DPL和段選擇符一起控制著訪問,同時,段選擇符結合偏移量(Offset)指出了中斷處理代碼的入口點。內核一般在門描述符中填入內核代碼段的段選擇符。一個中斷永遠不會將控制從高特權環轉向低特權環。特權級必須要麽保持不變(當內核自己被中斷的時候),或被提升(當用戶模式的代碼被中斷的時候)。無論哪一種情況,作為結果的CPL必須等於目的代碼段的DPL。如果CPL發生了改變,一個堆棧切換操作就會發生。如果中斷是被程序中的指令所觸發的(比如INT n),還會增加一個額外的檢查:門的DPL必須具有與CPL相同或更低的特權。這就防止了用戶代碼隨意觸發中斷。如果這些檢查失敗,正如你所猜測的,會產生一個一般保護錯(general-protection exception)。所有的Linux中斷處理器都以ring 0特權退出。

在初始化階段,Linux內核首先在setup_idt()中建立IDT,並忽略全部中斷。之後它使用include/asm-x86/desc.h的函數來填充普通的IDT表項(參見arch/x86/kernel/traps_32.c)。在Linux代碼中,名字中包含"system"字樣的門描述符是可以從用戶模式中訪問的,而且其設置函數使用DPL 3。"system gate"是Intel的陷阱門,也可以從用戶模式訪問。除此之外,術語名詞都與本文對得上號。然而,硬件中斷門並不是在這裏設置的,而是由適當的驅動程序來完成。

有三個門可以被用戶模式訪問:中斷向量3和4分別用於調試和檢查數值運算溢出。剩下的是一個系統門,被設置為SYSCALL_VECTOR。對於x86體系結構,它等於0x80。它曾被作為一種機制,用於將進程的控制轉移到內核,進行一個系統調用(system call),然後再跳轉回來。在那個時代,我需要去申請"INT 0x80"這個沒用的牌照 J。從奔騰Pro開始,引入了sysenter指令,從此可以用這種更快捷的方式來啟動系統調用了。它依賴於CPU上的特殊目的寄存器,這些寄存器存儲著代碼段、入口點及內核系統調用處理器所需的其他零散信息。在sysenter執行後,CPU不再進行特權檢查,而是直接進入CPL 0,並將新值加載到與代碼和堆棧有關的寄存器當中(cs,eip,ss和esp)。只有ring 0的代碼enable_sep_cpu()可以加載sysenter 設置寄存器。

最後,當需要跳轉回ring 3時,內核發出一個iretsysexit指令,分別用於從中斷和系統調用中返回,從而離開ring 0並恢復CPL=3的用戶代碼的執行。噢!Vim提示我已經接近1,900字了,所以I/O端口的保護只能下次再談了。這樣我們就結束了x86的運行環與保護之旅。感謝您的耐心閱讀。

參考:

http://blog.csdn.net/drshenlei/article/details/4265101

CPU Rings, Privilege, and Protection.CPU的運行環, 特權級與保護