Intel X86 CPU 系列的尋址方式與段式內存管理機制

轉自:http://blog.csdn.net/tianzhhy/article/details/5802192
分類: 操作系統原理 273人閱讀 評論(0)收藏 舉報

 

http://www.cnitblog.com/ygb/articles/8872.html

 

Refer to <<linux 內核源代碼情景分析 >> and <<Linux kernel Version:2.4.0>>

Having any problems, send mails to [email protected]

 

 

Intel X86 CPU 系列的尋址方式與段式內存管理機制

 

在 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 等。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章