【注意】雖然大家學習過X86匯編,對X86硬件架構有一定了解,但對X86保護模式和X86系統編程可能了解不夠。為了能夠清楚了解各個實驗中匯編代碼的含義,我們建議大家閱讀如下參考資料:
- 可先回顧一下lab0-manual中的“了解處理器硬件”一節的內容。
- 《Intel 80386 Reference Programmers Manual-i386》:第四、六、九、十章。在后續實驗中,還可以進一步閱讀第五、七、八等章節。
(1) 實模式
在bootloader接手BIOS的工作后,當前的PC系統處於實模式(16位模式)運行狀態,在這種狀態下軟件可訪問的物理內存空間不能超過1MB,且無法發揮Intel 80386以上級別的32位CPU的4GB內存管理能力。
實模式將整個物理內存看成分段的區域,程序代碼和數據位於不同區域,操作系統和用戶程序並沒有區別對待,而且每一個指針都是指向實際的物理地址。這 樣,用戶程序的一個指針如果指向了操作系統區域或其他用戶程序區域,並修改了內容,那么其后果就很可能是災難性的。通過修改A20地址線可以完成從實模式 到保護模式的轉換。有關A20的進一步信息可參考附錄“關於A20 Gate”。
(2) 保護模式
只有在保護模式下,80386的全部32根地址線有效,可尋址高達4G字節的線性地址空間和物理地址空間,可訪問64TB(有2^14個段,每個段 最大空間為2^32字節)的邏輯地址空間,可采用分段存儲管理機制和分頁存儲管理機制。這不僅為存儲共享和保護提供了硬件支持,而且為實現虛擬存儲提供了 硬件支持。通過提供4個特權級和完善的特權檢查機制,既能實現資源共享又能保證代碼數據的安全及任務的隔離。
【補充】保護模式下,有兩個段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一張段表可以包含8192 (2^13)個描述符[1],因而最多可以同時存在2 * 2^13 = 2^14個段。雖然保護模式下可以有這么多段,邏輯地址空間看起來很大,但實際上段並不能擴展物理地址空間,很大程度上各個段的地址空間是相互重疊的。目 前所謂的64TB(2^(14+32)=2^46)邏輯地址空間是一個理論值,沒有實際意義。在32位保護模式下,真正的物理空間仍然只有2^32字節那 么大。注:在ucore lab中只用到了GDT,沒有用LDT。
Reference: [1] 3.5.1 Segment Descriptor Tables, Intel® 64 and IA-32 Architectures Software Developer’s Manual
(3) 分段存儲管理機制
只有在保護模式下才能使用分段存儲管理機制。分段機制將內存划分成以起始地址和長度限制這兩個二維參數表示的內存塊,這些內存塊就稱之為段 (Segment)。編譯器把源程序編譯成執行程序時用到的代碼段、數據段、堆和棧等概念在這里可以與段聯系起來,二者在含義上是一致的。
分段機涉及4個關鍵內容:邏輯地址、段描述符(描述段的屬性)、段描述符表(包含多個段描述符的“數組”)、段選擇子(段寄存器,用於定位段描述符 表中表項的索引)。轉換邏輯地址(Logical Address,應用程序員看到的地址)到物理地址(Physical Address, 實際的物理內存地址)分以下兩步:
[1] 分段地址轉換:CPU把邏輯地址(由段選擇子selector和段偏移offset組成)中的段選擇子的內容作為段描述符表的索引,找到表中對應的段描述 符,然后把段描述符中保存的段基址加上段偏移值,形成線性地址(Linear Address)。如果不啟動分頁存儲管理機制,則線性地址等於物理地址。 [2] 分頁地址轉換,這一步中把線性地址轉換為物理地址。(注意:這一步是可選的,由操作系統決定是否需要。在后續試驗中會涉及。
上述轉換過程對於應用程序員來說是不可見的。線性地址空間由一維的線性地址構成,線性地址空間和物理地址空間對等。線性地址32位長,線性地址空間容量為4G字節。分段地址轉換的基本過程如下圖所示。
圖1 分段地址轉換基本過程
分段存儲管理機制需要在啟動保護模式的前提下建立。從上圖可以看出,為了使得分段存儲管理機制正常運行,需要建立好段描述符和段描述符表(參看bootasm.S,mmu.h,pmm.c)。
段描述符
在分段存儲管理機制的保護模式下,每個段由如下三個參數進行定義:段基地址(Base Address)、段界限(Limit)和段屬性(Attributes)。在ucore中的kern/mm/mmu.h中的struct segdesc 數據結構中有具體的定義。
- 段基地址:規定線性地址空間中段的起始地址。在80386保護模式下,段基地址長32位。因為基地址長度與尋址地址的長度相同,所以任何一個段都可以從32位線性地址空間中的任何一個字節開始,而不象實方式下規定的邊界必須被16整除。
- 段界限:規定段的大小。在80386保護模式下,段界限用20位表示,而且段界限可以是以字節為單位或以4K字節為單位。
- 段屬性:確定段的各種性質。
- 段屬性中的粒度位(Granularity),用符號G標記。G=0表示段界限以字節位位單位,20位的界限可表示的范圍是1字節至1M字節,增量為1字節;G=1表示段界限以4K字節為單位,於是20位的界限可表示的范圍是4K
- 段屬性中的粒度位(Granularity),用符號G標記。G=0表示段界限以字節位位單位,20位的界限可表示的范圍是1字節至1M字節,增量為1字節;G=1表示段界限以4K字節為單位,於是20位的界限可表示的范圍是4K字節至4G字節,增量為4K字節。
- 類型(TYPE):用於區別不同類型的描述符。可表示所描述的段是代碼段還是數據段,所描述的段是否可讀/寫/執行,段的擴展方向等。
- 描述符特權級(Descriptor Privilege Level)(DPL):用來實現保護機制。
- 段存在位(Segment-Present bit):如果這一位為0,則此描述符為非法的,不能被用來實現地址轉換。如果一個非法描述符被加載進一個段寄存器,處理器會立即產生異常。圖5-4顯示 了當存在位為0時,描述符的格式。操作系統可以任意的使用被標識為可用(AVAILABLE)的位。
- 已訪問位(Accessed bit):當處理器訪問該段(當一個指向該段描述符的選擇子被加載進一個段寄存器)時,將自動設置訪問位。操作系統可清除該位。
上述參數通過段描述符來表示,段描述符的結構如下圖所示:
圖2 段描述符結構
全局描述符表
全局描述符表的是一個保存多個段描述符的“數組”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR長48位,其中高32位為基地址,低16位為 段界限。由於GDT 不能有GDT本身之內的描述符進行描述定義,所以處理器采用GDTR為GDT這一特殊的系統段。注意,全部描述符表中第一個段描述符設定為空段描述符。 GDTR中的段界限以字節為單位。對於含有N個描述符的描述符表的段界限通常可設為8*N-1。在ucore中的boot/bootasm.S中的gdt 地址處和kern/mm/pmm.c中的全局變量數組gdt[]分別有基於匯編語言和C語言的全局描述符表的具體實現。
選擇子
線性地址部分的選擇子是用來選擇哪個描述符表和在該表中索引一個描述符的。選擇子可以做為指針變量的一部分,從而對應用程序員是可見的,但是一般是由連接加載器來設置的。選擇子的格式如下圖所示:
圖3 段選擇子結構
- 索引(Index):在描述符表中從8192個描述符中選擇一個描述符。處理器自動將這個索引值乘以8(描述符的長度),再加上描述符表的基址來索引描述符表,從而選出一個合適的描述符。
- 表指示位(Table Indicator,TI):選擇應該訪問哪一個描述符表。0代表應該訪問全局描述符表(GDT),1代表應該訪問局部描述符表(LDT)。
- 請求特權級(Requested Privilege Level,RPL):保護機制,在后續試驗中會進一步講解。
全局描述符表的第一項是不能被CPU使用,所以當一個段選擇子的索引(Index)部分和表指示位(Table Indicator)都為0的時(即段選擇子指向全局描述符表的第一項時),可以當做一個空的選擇子(見mmu.h中的SEG_NULL)。當一個段寄存 器被加載一個空選擇子時,處理器並不會產生一個異常。但是,當用一個空選擇子去訪問內存時,則會產生異常。
(4) 保護模式下的特權級
在保護模式下,特權級總共有4個,編號從0(最高特權)到3(最低特權)。有3種主要的資源受到保護:內存,I/O端口以及執行特殊機器指令的能 力。在任一時刻,x86 CPU都是在一個特定的特權級下運行的,從而決定了代碼可以做什么,不可以做什么。這些特權級經常被稱為為保護環(protection ring),最內的環(ring 0)對應於最高特權0,最外面的環(ring 3)一般給應用程序使用,對應最低特權3。在ucore中,CPU只用到其中的2個特權級:0(內核態)和3(用戶態)。
有大約15條機器指令被CPU限制只能在內核態執行,這些機器指令如果被用戶模式的程序所使用,就會顛覆保護模式的保護機制並引起混亂,所以它們被 保留給操作系統內核使用。如果企圖在ring 0以外運行這些指令,就會導致一個一般保護異常(general-protection exception)。對內存和I/O端口的訪問也受類似的特權級限制。
數據段選擇子的整個內容可由程序直接加載到各個段寄存器(如SS或DS等)當中。這些內容里包含了請求特權級(Requested Privilege Level,簡稱RPL)字段。然而,代碼段寄存器(CS)的內容不能由裝載指令(如MOV)直接設置,而只能被那些會改變程序執行順序的指令(如 JMP、INT、CALL)間接地設置。而且CS擁有一個由CPU維護的當前特權級字段(Current Privilege Level,簡稱CPL)。二者結構如下圖所示:
圖4 DS和CS的結構圖
代碼段寄存器中的CPL字段(2位)的值總是等於CPU的當前特權級,所以只要看一眼CS中的CPL,你就可以知道此刻的特權級了。
CPU會在兩個關鍵點上保護內存:當一個段選擇符被加載時,以及,當通過線性地址訪問一個內存頁時。因此,保護也反映在內存地址轉換的過程之中,既包括分段又包括分頁。當一個數據段選擇符被加載時,就會發生下述的檢測過程:
圖5 內存訪問特權級檢查過程
因為越高的數值代表越低的特權,上圖中的MAX()用於選擇CPL和RPL中特權最低的一個,並與描述符特權級(Descriptor Privilege Level,簡稱DPL)比較。如果DPL的值大於等於它,那么這個訪問可正常進行了。RPL背后的設計思想是:允許內核代碼加載特權較低的段。比如,你 可以使用RPL=3的段描述符來確保給定的操作所使用的段可以在用戶模式中訪問。但堆棧段寄存器是個例外,它要求CPL,RPL和DPL這3個值必須完全 一致,才可以被加載。下面再總結一下CPL、RPL和DPL:
- CPL:當前特權級(Current Privilege Level) 保存在CS段寄存器(選擇子)的最低兩位,CPL就是當前活動代碼段的特權級,並且它定義了當前所執行程序的特權級別)
- DPL:描述符特權(Descriptor Privilege Level) 存儲在段描述符中的權限位,用於描述對應段所屬的特權等級,也就是段本身真正的特權級。
- RPL:請求特權級RPL(Request Privilege Level) RPL保存在選擇子的最低兩位。RPL說明的是進程對段訪問的請求權限,意思是當前進程想要的請求權限。RPL的值由程序員自己來自由的設置,並不一定 RPL>=CPL,但是當RPL<CPL時,實際起作用的就是CPL了,因為訪問時的特權檢查是判斷:max(RPL,CPL)& lt;=DPL是否成立,所以RPL可以看成是每次訪問時的附加限制,RPL=0時附加限制最小,RPL=3時附加限制最大。
原文地址:https://objectkuan.gitbooks.io/ucore-docs/content/lab1/lab1_3_2_1_protection_mode.html