轉自:http://blog.csdn.net/tianzhhy/article/details/5802192
分類:
作業系統原理
2010-08-10 18:00
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
viloner@163.com
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 等。