在 X86 系列中, 8086 和 8088 是 16 位處理器,而從 80386 開始爲 32 位處理器, 80286 則是系列從 8088 到 80386, 也就是從 16 位到 32 位過渡的一箇中間步驟。 80286 雖然還是 16 位處理器,可是在尋址方式上開始了從「初地址模式」到「保護模式」的過渡。
當咱們說一個 CPU 是「 16 位」或「 32 位」時,指的是處理器中「自述邏輯單元」 (ALU) 的寬度。系統總線中的數據線部分,稱爲「數據總線」,一般與 ALU 具備相同的寬度 ( 但有例外 ) 。那麼「地址總線」的寬度呢?最天然的地址總線寬度是與數據總線一致。這是由於從程序設計的角度來講,一個地址,也就是一個指針,最好是與一個整數的長度一致。可是若是從 8 位 CPU 尋址能力的角度來考慮,則其實是不現實的,由於一個 8 位的地址只能用來尋訪 256 個不一樣的地址單元,這顯然過小了。因此,通常 8 位 CPU 的地址總線都是 16 位的。但 16 位仍是過小。 Intel 決定在其 16 位 CPU ,即 8086 中採用 1M 字節的內存地址空間,地址總線的寬度也就相應地肯定了,那就是 20 位。但這樣就出現了一個問題,雖然地址總線的寬度是 20 位,但 CPU 中 ALU 的寬度卻只有 16 位,也就是說直接加以運算的指針長度是 16 位的。如何來填補這個空隙呢? Intel 設計了一種在當時看來不失巧妙的方法,即分段的方法。
Intel 在 8086CPU 中設置了四個「段寄存器」: CS 、 DS 、 SS 和 ES ,分別用於可執行代碼即指令、數據、堆棧和其餘。每一個段寄存器都是 16 位,對應於地址總線中的高 16 位。每條「訪內」指令中的「內部地址」都是 16 位的,可是在送上地址總線以前在 CPU 內部自動地與某個段寄存器中的內容相加,造成一個 20 位的實際地址。這樣,就實現了從 16 位內部地址到 20 位實際地址的轉換,或者「映射」。這裏要注意段寄存器中的內容對應於 20 位地址總線中的高 16 位,因此在相加時其實是拿內部地址中的高 12 位與段寄存器中的 16 位相加,而內部地址中的低 4 位保持不變。但這種方法是有缺陷的,主要是沒有地址空間保護機制。對於每個由段寄存器的內容肯定的「基地址」,一個進程老是可以訪問今後開始的 64K 字節的連續地址空間,而沒法加以限制。同時,能夠用來改變段寄存器內容的指令也不是什麼「特權指令」,也就是說,經過改變段寄存器的內容,一個進程能夠爲所欲爲地訪問內存中的任何一個單元,而絲絕不受限制。不能對一個進程的內存訪問加以限制,也就談不上對其餘進程以及系統自己的保護。與此相應,一個 CPU 若是缺少對內存訪問的限制,或者說保護,就談不上什麼內存管理,也就談不上是現代意義上的中央處理器。因爲 8086 的這種內存尋址方式缺少對內存空間的保護,因此爲了區別於後來出現的「保護模式」,就稱爲「實地址模式」。
針對 8086 的這種缺陷, Intel 從 80286 開始實現其「保護模式」。同時不久後 32 位的 80386CPU 也開發成功了。這樣,從 8088/8086 到 80386 就完成了一次從比較原始的 16 位 CPU 到現代的 32 位 CPU 的飛躍,而 80286 則變成此次飛躍的一箇中間步驟。
80386 是個 32 位 CPU ,也就是說它的 ALU 數據總線是 32 位的,則最天然的地址總線寬度也應是與數據總線一致的。當地址總線的寬度達到 32 位時,其尋址能力達到了 4G ,對於內存來講彷佛是足夠了。因此,若是新設計一個 32 位 CPU 的話,其結構應該是能夠作到很簡潔,很天然的。可是, 80386 卻沒法作到這一點。做爲一個產品系列中的一員, 80386 必須維持那些段寄存器,還必須支持實地址模式,在此同時又要支持保護模式。所以, Intel 決定在段寄存器的基礎上構築保護模式,而且保留段寄存器爲 16 位 ( 這樣才能夠利用原有的四個段寄存器 ) ,可是卻又增添了兩個段寄存器 FS 和 GS 。爲了實現保護模式,光是用段寄存器來肯定一個基地址是不夠的,至少還要有一個地址段的長度,而且還須要一些其餘信息,如訪問權限之類。因此,這裏須要的是一個數據結構,而並不是一個單純的基地址。對此, Intel 設計人員的基本思路是:在保護模式下改變段寄存器的功能,使其從一個單純的基地址變成指向這樣一個數據結構的指針。所以,當一個訪存指令發出一個內存地址時, CPU 按照下面過程實現從指令中的 32 位邏輯地址到 32 位物理地址的轉換:
1. 首先根據指令的性質來肯定該使用哪個段寄存器,例如轉移指令中的地址在代碼段,而數據指令中的地址在數據段。這一點與實地址模式相同。
2. 根據段寄存器的內容,找到相應的 「 段描述結構 」 。
3. 從 「 段描述結構 」 中獲得基地址。
4. 將指令中的地址做爲位移,與段描述結構中規定的段長度相比,看是否越界;
5. 根據指令的性質和段描述符中的訪問權限來肯定是否越權;
6. 最後纔將指令中的地址做爲位移,與段基地址相加,獲得物理地址。
雖然段描述結構存儲在內存中,在實際使用時卻將其裝載入 CPU 中的一組「影子」結構,而 CPU 在運行時則使用其在 CPU 中的「影子」。從保護的角度考慮,在由 ( 指令給出的 ) 內部地址 ( 或者說「邏輯地址」 ) 轉換成物理地址的過程當中,必需要在某個環節上對訪問權限時行比對,以訪止不具備特權的用戶程序經過玩弄某些詭計 ( 例如修改段寄存器的內容,修改段描述結構的內容等 ) ,得以非法訪問其餘進程的空間或系統空間,從而實現了保護。
明白了這個思路, 80386 的段式內存管理機制就比較容易理解了,下面就是此機制的實際實現。
首先,在 80386CPU 中增設了兩個寄存器:一個是全局性段描述表寄存器 GDTR ,另一個是局部性段描述表寄存器 LDTR ,分別能夠用來指向存儲在內存中的一個段描述結構數組,或者稱爲段描述表。因爲這兩個寄存器是新增設的,不存在與原有的指令是否兼容的問題,訪問這兩個寄存器的專用指令便設計成「特權指令」。
在此基礎上,段寄存器的高 13 位用做訪問段描述表中具體描述結構的下標 (index) ,以下圖所示
段寄存器定義
RPL :請求特權級, 2 位二進制數字,求特權級是將要訪問的段的特權級。
TI :表指示符。爲 0 時,從 GDT 中選擇描述符;爲 1 時,從 LDT 中選擇描述符。
Index :索引。指出要訪問描述符在段描述符表中的順序號。總共有 213=8192 個。
GDTR 或 LDTR 中的段描述表指針和段寄存器中給出的下標結合在一塊兒,才決定了具體的段描述表項在內存中的什麼地方,也能夠理解成,將段寄存器內容的低 3 位屏蔽掉之後與 GDTR 或 LDTR 中的基地址相加獲得描述表項的起始地址。所以就沒法經過修改描述表項的內容來玩弄詭計,從而起到保護的做用。每一個段描述表項的大小是 8 個字節,每一個描述表項含有段的基地址和段的大小,再
加上其餘一些信息,其結構以下圖所示:
8 字節段描述表項的含義
結構中的 B31-B24 和 B23-B16 分別爲基地址的 bit16~bit23 和 bit24~bit31. 而 L19~L16 和 L15~L0 則爲段長度 (limit) 的 bit0~bit15 和 bit16~bit19.
G :粒度位。
G=1 時,限長以頁爲單位;
G=0 時,限長以字節爲單位。
D :默認操做數寬度。
D=1 時,爲 32 位數據操做段;
D=1 時,爲 16 位數據操做段。
AVL :可用位。
這一位保留給操做系統或應用程序來使用
DPL 是個 2 位的位段,而 TYPE 是一個 4 位的位段。它們的定義以下:
P :存在位
等於 1 時表示該段己裝入內存;
等於 0 時表示該段沒有在內存中,訪問這個段會產生段異常。 n
DPL :描述符特權級,說明這個段的特權級
S :描述符類型位
爲 1 時,這個段爲代碼段、數據段或堆棧段;
爲 0 時,爲系統段描述符。
E :可執行位,區分代碼段和數據段
S=0 且 E=1 時,這是一個代碼段,可執行。
S=0 且 E=0 時,這是一個數據段或堆棧段,不可執行。
E=0 時,後面的兩位爲 ED 和 W ;
若 E=1 時,後面的兩位爲 C 和 R 。
ED :擴展方向位
爲 0 時,段從低地址向高地址擴展,偏移量小於等於限長。
爲 1 時,段從高地址向低地址擴展,偏移量必須大於限長。
W :寫容許位
爲 0 時,不容許對這個數據段寫入;
爲 1 時,容許對這個數據段寫入。
C :一致位
爲 0 時,這個段不是一致代碼段
爲 1 時,這個段是一致代碼段
R :讀容許位
爲 0 時,不容許讀這個段的內容
爲 1 時,容許讀這個段的內容
A :訪問位
爲 1 表示段已被訪問過
爲 0 表示段未被訪問過。
也能夠用一段「僞代碼」來講明整個段描述結構:
段描述結構 : typedef struct { unsigned int base_24_31:8; // 基地址最高 8 位 unsigned int g:1; //granularity 表段長度單位 [0] 字節 [1]4KB unsigned int d_b:1; //default operation size 存取方式 [0]16 位 [1]32 位 unsigned int unused:1; // 固定設置成 0 unsigned int avl:1 //avaliable, 可供系統軟件使用 unsigned int seg_limit_16_19:4; // 段長度的最高 4 位 unsigned int p:1; //segment present, [0] 該段的內容不在內存中 unsigned int dp1:2; //Descriptor privilege level, 訪問本段所需權限 unsigned int s:1; // 描述項類型 [1] 系統 [0] 代碼 / 數據 unsigned int type:4 // 段的類型 , 與 S 標誌位一塊兒使用 unsigned int base_0_23:24; // 基地址的低 24 位 unsigned int seg_limit_0_15:16; // 段長度的低 16 位 }descriptor; 以這裏的位段 type 爲例,「: 4 」表示其寬度爲 4 位。整個數據結構的大小爲 64 位,即 8 個字節。 在讀寫內存單元時, CPU 須要檢查段描述符的內容是否和當前操做相一致, CPU 的運行效率極大地下降。爲解決這個問題, CPU 在內部設置了段描述符高速緩存,能夠看做是對段寄存器的擴充。擴充後的段寄存器分紅兩部分,一部分是可見的 ( 對程序而言 ) ,還與原來的段寄存器同樣,另外一部分是不可見的,就是用來放影子描述項的空間,這一部分是專供 CPU 內部使用的。在指令執行過程當中,只有段寄存器的值發生改變時,才須要到 GDT 或 LDT 中裝入段描述符。若是段寄存器的值不改變,高速緩存 ( 即對段寄存器擴充的那部分 ) 中的段描述符能夠被直接引用,這樣就避免了到主存中頻繁讀取段描述符。提升了 CPU 的效率。 在 80386 的段式內存管理的基礎上,若是把每一個段寄存器都指向同一個描述項,而在該描述項中則將基地址設成 0, 並將段長度設成最大,這樣便造成一個從 0 開始覆蓋整個 32 位地址空間的一個整段。因爲基地址爲 0, 此時的物理地址與邏輯地址相同, CPU 放到地址總線上去的地址就是在指令中給出的地址。這樣的地址有別於由「段寄存器 / 位移量」構成的「層次式」地址,因此 Intel 稱其爲「平面 (Flat) 」地址。 Linux 內核的源代碼 ( 更確切地應該是 gcc) 採用平面地址。這裏要指出,平面地址的使用並不意味着繞過了段描述表、段寄存器這一整套段式內存管理的機制,而只是段式內存管理的一種使用特例。 利用 80386 對段式內存管理的硬件支持,能夠實現段式虛存管理。如前所述,當一個段寄存器內容改變時, CPU 要根據新的段寄存器內容以及 GDTR 或 LDTR 的內容找到相應的段描述項並將其裝入 CPU 中。在些過程當中, CPU 會檢查該描述項中的 p 標誌位 ( 表示「 present 」 ) ,若是 p 標誌位爲 0, 就表示該描述項所指向的那一段內容不在內存中 ( 也就是說,在磁盤上的某個地方 ) ,此時 CPU 會產生一次異常 (exception ,相似於中斷 ) ,而相應的服務程序即可以從磁盤交換區將這一段的內容讀入內存中的某個地方,並據此設置描述項中的基地址,再將 p 標誌位設置成 1. 相應地,內存中暫時不用的存儲段則能夠寫入磁盤,並將其描述項中的 p 標誌位改爲 0. 對段式內存管理的支持只是 i386 保護模式的一個組成部分。若是沒有系統狀態和用戶狀態的分離,以及特權指令 ( 只容許在系統狀態下使用的 ) 的設立,那麼儘管有了前述的段式內存管理,也還不能起到保護的效果。前面已提到特權指令的設置,若是來裝入和存儲 GDTR 和 LDTR 的指令 LGDT/LLDT 和 SGDT/SLDT 等就都是特權指令。正是因爲這些特權指令都只能在系統狀態 ( 也就是在操做系統的內核中 ) 使用,才使得用戶程序不但不能改變 GDTR 和 LDTR 的內容,還由於既沒法確知其段描述表在內存中的位置,又沒法訪問其段描述表所在的空間 ( 只能在系統狀態下才能訪問 ) ,從而沒法經過修改段描述項來打破系統的保護機制。那麼, 80386 怎麼來分隔系統狀態和用戶狀態,而且提供在兩種狀態之間切換的機制呢? 80386 並不僅是像通常 CPU 一般所作的那樣,劃分出系統狀態和用戶狀態,而是劃分紅四個特權級別,其中 0 級爲最高, 3 級爲最低。每一條指令也都有其適用的級別,如前所述的 LGDT ,就只有在 0 級的狀態下才能使用,而通常的輸入 / 輸出指令 (IN , OUT) 則規定爲 0 級或 1 級。一般,用戶的應用程序都是 3 級。通常程序的當前運行級別由其代碼段的局部描述項 ( 即由段寄存器 CS 所指向的局部段描述項 ) 中的 dpl 字段決定 (dpl 表示「 descriptor privilege level 」 ) 。固然,每一個描述項的 dpl 字段都是從 0 級狀態下由內核設定的。而全局段描述的 dpl 字段,則又有所不一樣,它是表示所需的級別。 前面講過, 16 位的段寄存器中的高 13 位用做下標來訪問段描述表,而低 3 位是幹什麼的呢?下面經過一段僞代碼來講明: typedef struct { unsignedshort seg_idx: 13; /*13 位的段描述項的下標 */ unsignedshort ti: 1; /* 段描述表指示位, 0 表示 GDT , 1 表示 LDT*/ unsignedshort rpl: 2; /*Requested Privilege Level, 要求的優先級別 */ } 段寄存器 ; 當段寄存器 CS 中的 ti 位爲 1 時,表示要使用全局段描述表,爲 0 時,則表示要使用局部段描述表而 rpl 則表示所要求的權限。當改變一個段寄存器的內容時, CPU 會加以檢查,以確保該段程序的當前執行權限和段寄存器所指定要求的權限均不低於所要訪問的那一段內存的權限 dpl 。 至於怎樣在不一樣的執行權限之間切換,將在進程高度、系統調用和中斷處理中討論。此外,除了全局段描述表指針 GDTR 和局部段描述表指針 LDTR 兩個寄存器外,其實 i386CPU 中還有個中斷向量表指針寄存器 IDTR 、與進程 ( 在 Intel 術語中稱爲「任務」, Task) 有關的寄存器 TR 以及描述任務狀態的「任務狀態段」 TSS 等。 [url]http://www.cnitblog.com/ygb/articles/8872.html[/url] Refer to <<linux 內核源代碼情景分析 >> and <<Linux kernel Version:2.4.0>> Having any problems, send mails to [email]viloner@163.com[/email]