作業系統實踐之第二章(保護模式下的分段定址)
理論準備:
真實模式下的定址相信大家已經很清楚了,它分為兩個部分,一部分是段基址,另一部分是段內偏移。段基址由段暫存器值左移4位表示,段內偏移則記錄了相對於某個段起始位置的偏移量,將這兩個值相加就得到了所需的實體地址。
在保護模式下,相對來說其定址方式就比較複雜了。嗯,本次先不討論分頁的問題,我們主要把注意力集中在分段上(我還是比較喜歡步步為營)。前面很多地方使用了選擇子的概念,那麼它究竟是一個什麼東東呢?它的結構如下圖所示:
若TI位為0,則在GDT中查詢對應的描述符;若TI位為1,在LDT中查詢對應的描述符。RPL(RequestedPrivilege Level)是通過段選擇子的第0位和第1位表現出來的。處理器通過檢查RPL和CPL(Current Privilege Level,當前執行程式或任務的特權等級)來確認一個訪問請求是否合法。而剩下的13位用於指定相應的描述符在GDT或LDT表中的索引。
描述符的結構如下圖示:
除了段基址、段界限和段屬性之外,還有其它一些描述符中的屬性含義如下:
段基址:由上圖中的兩部分(BASE 31-24 和 BSE23-0)組成
G位:段界限粒度位,該位為 0 表示單位是位元組,為1表示單位是 4KB
D/B: 該位分為3中情況。
1. 在可執行程式碼段描述符中,這一位叫做D位。D= 1時,預設情況下指令使用32位地址及32位或8位運算元;D= 0時,預設情況下使用16位地址及16位或8位運算元。
2. 在向下擴充套件資料段的描述符中,這一位叫做B位。B= 1時,段的上部界限為4GB;B= 0時,段的上部界限為64KB。
3. 在描述堆疊段(由SS暫存器指向的段)的描述符中,這一位叫做B位。B= 1時,隱式的堆疊訪問指令(push、pop、call等)使用32位堆疊暫存器esp;D= 0時,隱式的堆疊訪問指令使用16位堆疊指標暫存器sp。
AVL: 保留位,可以被系統軟體使用。
段界限:單位由 G 位決定。數值上(經過單位換算後的值)等於段的長度(位元組)- 1。
P位: 段存在位,該位為 0 表示該段在記憶體中不存在,為 1 表示在記憶體中存在。
DPL:描述符特權等級,可以使0/1/2/3,數字越小特權級越大。
S位: 該位為 1 表示這是一個數據段或者程式碼段。為 0 表示這是一個系統段(比如呼叫門,中斷門等)
TYPE: 根據 S 位的結果,再次對段型別進行細分。
TYPE值 |
資料段/程式碼段 |
系統段/門描述符 |
0 |
只讀 |
<未定義> |
1 |
只讀,已訪問 |
可以286TSS |
2 |
讀/寫 |
LDT |
3 |
讀/寫,已訪問 |
忙的286TSS |
4 |
只讀,向下擴充套件 |
286呼叫門 |
5 |
只讀,向下擴充套件,已訪問 |
任務門 |
6 |
讀/寫,向下擴充套件 |
286中斷門 |
7 |
讀/寫,向下擴充套件,已訪問 |
286陷阱門 |
8 |
只執行 |
<未定義> |
9 |
只執行,已訪問 |
可用386TSS |
A |
執行/讀 |
<未定義> |
B |
執行/讀,已訪問 |
忙的386TSS |
C |
只執行,一致碼段 |
386呼叫門 |
D |
只執行,一致碼段,已訪問 |
<未定義> |
E |
執行/讀,一致碼段 |
386中斷門 |
F |
執行/讀,一致碼段,已訪問 |
386陷阱門 |
保護模式下不再像真實模式那樣提供段地址。在原來放段地址的段暫存器裡含有一個選擇子(selector),用於選擇描述表內的一個描述符。描述符(descriptor)描述儲存器段的位置、長度和訪問許可權。
由於段暫存器和偏移地址仍然用於訪問儲存器,所以保護模式指令和真實模式指令是完全相同的。事實上,很多為在真實模式下執行編寫的程式,不用更改就可在保護模式下執行。兩種模式之間的區別是微處理器訪問儲存段時對段暫存器的解釋不同。
假設不啟用分頁機制,那麼段選擇符和段內偏移地址是如何找到最後的實體地址的呢?我們以在GDT中查詢相應的段描述符為例:
首先,我們會有一個語句用於載入gdtr暫存器,該暫存器的結構如下圖所示:
其中有GDT的基址及界限,這樣就可以訪問相應的GDT了。
接著,把相應的選擇子選到合適的暫存器處。
最後,當使用(段基址:段內偏移)的形式進行定址的時候,會將段暫存器中選擇子的高13位作為描述符索引在GDT中查詢相應的描述符。而描述符中記載著該段的基址,再配合相應的段內偏移即可找出該線性地址,即實體地址(不分頁時)。
例項講解:
為了能夠徹底理解選擇子的功效,我們給出以下示例程式碼,看完之後要是還不懂我選擇狗帶╰( ̄▽ ̄)╭
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 屬性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致程式碼段, 16
LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
; GDT 結束
GdtLen equ $ - LABEL_GDT ; GDT長度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
上面這段程式碼定義了GDT,按照前面的順序,我們再看載入gdtr暫存器相關的內容:
; 為載入 GDTR 作準備
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 載入 GDTR
lgdt [GdtPtr]
經過上面這段程式碼gdtr的內容剛好滿足了該暫存器所需的格式(低16位由語句:GdtPtr dw GdtLen-1給出GDT界限;而高32位則由上面這段程式碼設定為了GDT的基址,最後以mov dword [GdtPtr + 2], eax 語句填入了原本為0的地址空間中)。好了,至此gdtr暫存器相關的操作就完成了。
我們總說按照選擇子的索引來查詢GDT,那麼此時被指向的GDT中的索引又是怎樣的呢?我們看到GDT開始時首先定義了一個空描述符(後面可以看到,這個空描述符很關鍵),理所當然,GDT中所有的描述符所對應的在描述表中的“索引”就是它們在GDT中的定義順序。也即有,索引0:空描述符,索引1:Normal描述符,索引2:非一致程式碼段,索引3:Data。
一般我們會以如下的方式來定義選擇子:
; GDT 選擇子
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
其實就這一小段程式碼來看說它是偏移也沒有錯,只是其中還有更玄妙的關係隱藏在裡面。我們知道每個描述符的大小都是8位元組,那麼上面給出的選擇子就都是8的倍數。有沒有聯想到什麼?選擇子的低3位是與索引描述符操作無關的,3位哦,在選擇子(或者說是偏移量)中去掉低三位後發生了什麼?是的,恰好就是對應的描述符索引!查到描述符後的操作相信大家就都懂了,其中有段基址,手上還有段內偏移,那還說什麼,跑都跑不掉。你跑啊,你越跑我就越興奮︿( ̄︶ ̄)︿