1. 程式人生 > >作業系統中的描述符和GDT

作業系統中的描述符和GDT

在作業系統中,全域性描述符是什麼?GDT又是什麼?在進入保護模式之前,準備好GDT和GDT中的描述符是必須的嗎?用匯編程式碼怎麼建立描述符?本文解答上面幾個問題。 在真實模式下,CPU是16位的,意思是,暫存器是16位的,陣列匯流排(data bus)是16位的,但地址匯流排是20位的。實體記憶體地址的計算公式是: $$ 實體地址 = 段地址 * 16 + 偏移量 $$ 段地址和偏移量都是16位的,能定址的最大記憶體地址是1M。 1M是怎麼計算出來的?2的20次方就是1M,能表示的記憶體地址是 0~(2的20次方-1)。用簡單例子來理解,1位十進位制數能表示的最大數是10 - 1 = 9,但1位十進位制數能表示的數卻是 `0` ,和 `1-9` ,總計10個數字。 若一個記憶體地址是`20:30`,最終記憶體地址是:`20 * 16 + 30`。 在保護模式下,記憶體地址仍然用“段地址:偏移量”的方式來表示。不過,“段地址”的含義不同於真實模式下的“段地址”。在真實模式下,段地址是實體地址的高地址部分,具體說,是高16位部分。而在保護模式下,段地址是選擇子,指向一個結構,這個結構描述了一個記憶體區域,告知該區域的記憶體地址從哪裡開始,在哪裡結束,還告知了這片記憶體能不能被訪問、能不能被讀取等資料。這個結構組成一個集合,叫GDT,而這個結構叫GDT項,它有一個術語,叫“描述符”。 GDT的作用是提供段式儲存機制。段式儲存機制由段暫存器和GDT共同提供。段暫存器提供段值,即描述符在GDT中的索引,也就是選擇子。根據選擇子在GDT中找到目標描述符。這個描述符中包含一段記憶體的初始地址、這段記憶體的最大地址、這段記憶體的屬性。 #### GDT的構成 GDT項即全域性描述符的長度是8個位元組,64個bit,64個位,0~63位,而不是1~64位。下圖是寫了位編號的8個位元組。真實的全域性描述符是不折行的,這裡無法在一行顯示全部資料,因此折行了。 ```shell 63|62|61|60|59|58|57|56| 55|54|53|52|51|50|49|48| 47|46|45|44|43|42|41|40| 39|38|37|36|35|34|33|32| 31|30|29|28|27|26|25|24| 23|22|21|20|19|18|17|16| 15|14|13|12|11|10|09|08| 07|06|05|04|03|02|01|00| ``` `15|14|13|12|11|10|09|08| 07|06|05|04|03|02|01|00|`。段界限1。段界限的 0~15 位。描述符的 0~15 位。 `39|38|37|36|35|34|33|32| 31|30|29|28|27|26|25|24| 23|22|21|20|19|18|17|16|`。段基址1。段基址的 0~23位。描述符的 16~39位。 `55|54|53|52|51|50|49|48| 47|46|45|44|43|42|41|40|`。很複雜,很碎片化,需進一步放大觀察。 `43|42|41|40|`,TYPE。4位。 `44`,S。是否為系統段(待驗證)。1位。 `46|45`,DPL。2位。 `47`,P。1位。 上面是一個位元組,下面是第二個位元組。 `51|50|49|48|`。段界限2。段界限的第 16~19 位。描述符的第 48~51 位。段界限一共有20位。 `52`。AVL。1位。 `53`。0。1位。 `54`。D/B。1位。 `55`。G。1位。 段屬性佔用空間的位數:4 + 1 + 2 + 1 + 1 + 1 * 3 = 12。 `63|62|61|60|59|58|57|56|`。段基址2。段基址的第 24~31 位。描述符的第 56~63 位。段基址一共有32位。 描述符的結構比較複雜,要記住它,有點困難,不過並非不可能記住。作者覺得沒有必要一個位元組不差地背誦出來。 #### 選擇子 描述符的選擇子的長度是16位。 ```shell 15|14|13|12|11|10|09|08| 07|06|05|04|03|02|01|00| ``` `01|00|`,RPL。 `02`,T1。 `15|14|13|12|11|10|09|08| 07|06|05|04|03`,描述符在GDT中的索引。 #### 段式儲存機制的定址方式 段地址儲存的是描述符的選擇子,根據選擇子能找到GDT中對應的描述符。從描述符中獲取段基址,然後加上段式儲存機制中的偏移量,就是線性地址。在當前語境下,線性地址等同實體地址。 #### 概念比較 邏輯地址。段式機制的地址,例如“段地址:偏移量”,就是邏輯地址。 線性地址。在保護模式下,用邏輯地址中的段地址從GDT中找到描述符,然後從描述符中獲取段的基址,段基址加上偏移量的結果就是線性地址。 如上文所言,線性地址目前可視為實體地址。開啟分頁機制後,線性地址不能等同於實體地址。實體地址是實體記憶體的一個編號。 #### 作者的疑問 進入保護模式前,為什麼需要建立好描述符、選擇子、GDT?這些是必要條件嗎? 作者曾認為這些不是必須。再次瞭解段式儲存機制後,改變了看法:進入保護模式前,必須準備好GDT、描述符和描述符選擇子。這是由保護模式下的記憶體定址方式決定的。 無論是在真實模式下還是保護模式下,都需要使用記憶體。在保護模式下,怎麼找到某片記憶體呢?保護模式下,使用段式機制。回憶一下,段式儲存機制的定址方式是: $$ 段地址(選擇子)-----》在GDT中找到描述符----》在描述符中找到段基址----》段基址+偏移量 = 線性地址 $$ 不在進入保護模式前準備好選擇子、GDT、描述符,就無法在保護模式中使用記憶體。 作者還有一個疑問:上面的定址過程是CPU自動完成的嗎? #### 實現描述符 ##### C語言 ###### 描述符 下面內容的前提是,32位CPU。 ```c struct { int segmentLimit1:16; // 段界限1 int segmentBaseAddress1:24; // 段基址1 char attributeType:4; // 段屬性,TYPE char attributeS:1; // 段屬性,S char attributeDPL:2; // 段屬性,DPL char attributeP:1; // 段屬性,P char segmentLimit2:4; // 段界限2 char attributeAVL:1; // 段屬性,AVL char attributeZero:1; // 段屬性,值為0 char attributeDB:1; // 段屬性,DB char attributeG:1; // 段屬性,G char segmentBaseAddress2; // 段基址2 }GlobalDescriptor; ``` 上面的用法是錯誤的。對位域的使用是錯的,換成int來使用位域也無能力寫正確,因為太麻煩。在這個知識點耗費了不少時間。 參考書中程式碼後,寫出下面的程式碼: ```c struct{ unsigned short segmentLimitLow; // 段界限1,16位,0~15 位。描述符的第 0~15 位。 unsigned short segmentBaseAddressLow; // 段基址低16位,0~15 位。描述符的第 16~31 位。 unsigned char segmentBaseAddressMid; // 段基址 16~23 位。描述符的第 32~39 位。 unsigned char attribute; // 段屬性。描述符的第 40~47 位。 unsigned char segmentLimitHight_attribute2; // 段界限 16~19 位,第 20~23 位是段屬性。描述符的第 48~55 位。 unsigned char segmentBaseAddressHigh; // 段基址 24~31 位。描述符的第 46~63 位。 }GlobalDescriptor; ``` 段基址雖然儲存在描述符的第 16~39 位 和第 24~31 位兩段連續的空間中,但用C語言表示它的時候,卻人為地將它拆分成了“低位”、“中位”和“高位”三部分,也就是,把描述符的第 16~39 位拆分成了第 16~31 位和第 32~39 位兩段。在C語言中,沒有現成的能儲存23位的整數型別,卻用能儲存16位和8位的整數型別。將段基址連在一起的24位拆分,用C語言表示更方便。 ###### C語言中的位域 前面已經用到了位域,那就簡單學習一下位域的知識吧。 用兩段程式碼開始。 ```c struct{ unsigned int age; unsigned int height; }Person; ``` ```c struct{ unsigned int age:3; unsigned int height:4; }Person2; ``` 第二段程式碼使用了位域,第一段程式碼是普通的`struct`結構。位域語法與`struct`的差異僅在於宣告成員變數的語法不同。 `struct`結構中,宣告成員變數的語法是`unsigned int age`。在位域中,宣告成員變數的語法是`unsigned int age:3`。後者指定了成員變數使用的bit的數量,是3個,而不是1個位元組、8個bit。 第一段程式碼建立的`Person`佔用8個位元組,第二段程式碼建立的`Person2`佔用4個位元組。 抽象出位域的成員變數的宣告語法:`dataType VariableName:bitCount`。`dataType`只能是`int`系列的整數型別,即只能是`int`、`unsigned int` 和 `signed int` 三種類型,不能是`char`等型別。這是語法規定。`bitCount`不能超過8個位元組。 ##### nasm彙編 用匯編語言表示描述符,是作者寫本文的終極目的,前面的一切都是鋪墊和基礎。C語言表示描述符,在前面寫出來,是因為它是作者理解描述符的彙編程式碼的大功臣。作者在看描述符的彙編程式碼前,沒有學過組合語言,所以第一次看描述符的彙編程式碼時,怎麼都理解不了。看了別人寫的描述符的C語言程式碼後,才恍然大悟,突然理解了描述符的的彙編程式碼。 所以,在前文給出描述符的C程式碼,一是為了紀念這個大功臣,二是讓與曾經看不懂彙編程式碼的作者一樣的讀者也能借住C程式碼理解彙編程式碼。當然,可能是作者多慮了,讀者朋友才不會像作者這麼愚鈍呢。 ###### 不使用巨集 第一個問題,建立一個描述符,例如`DESC_VIDEO`,語法是什麼樣的。 第二個問題。描述符的實質是段基址、段界限和段屬性。是直接用程式碼堆砌出描述符呢還是根據給定的段基址、段界限和段屬性經過運算拼湊出描述符? 先解答第二個問題。直接用程式碼堆砌出描述符的彙編程式碼如下: ```assembly DESC_VIDEO dw 3120h ; 描述符的第 0~15 位 dw 111Fh ; 描述符的第 16~31 位 db EFh ; 描述符的第 32~39 位 db 42h ; 描述符的第 40~47 位 db 00h ; 描述符的第 48~55 位 db FFh ; 描述符的第 56~63 位 ``` 與前面的C程式碼比較,每行對應一個struct的成員變數。從上面的彙編程式碼能看出段基址、段界限和段屬性是什麼嗎?看不出來,需要計算。而且,總不能拿到給定的段基址、段界限和段屬性後,先將它們轉換成二進位制,然後再分割填到上面的程式碼中吧?最好是給定段基址、段界限和段屬性後,經過一段程式碼處理,就自動構建了描述符。這就是下面要寫的方式。 ###### 巨集 彙編中的巨集類似C語言中的函式,給定引數,函式會完成一些功能。這個巨集接收段基址、段界限和段屬性,然後生成描述符。 巨集的語法是什麼樣的?建立一個巨集的模板是: ```assembly %macro macroName paramCount ;some code ;some code %endmacro ``` 建立描述符的巨集是: ```assembly ; 三個引數依次是:base(段基址)、limit(段界限)、attribute(段屬性) ; 在巨集中需用到這三個引數時,對應的代號分別是:%1、%2、%3。 ; base--32位,limit--20位,attribute--12位 %macro Descriptor 3 dw %2 & FFFFh ; 段界限的第 0~15 位。16位 dw %1 & FFFFh ; 段基址的第 0~15 位。16位。 db (%1 & FF0000h) >> 16 ; 段基址的第 16~23 位。8位。 db %3 & FFh ; 段屬性的第 0~7 位。48位。 db (%2 & F0000) | (%3 >> 8) ; 段界限的第 16~20 位 和 段屬性的第 8~11 位。56位。 db %1 >> 24 ; 段基址的第 24~31 位。8位。 %endmacro ``` 使用這個巨集建立一個描述符,程式碼如下: ```assembly DESC_VIDEO: Descriptor 0B8000h 0ffff 0 ``` 段屬性是隨便設定的。描述符的段屬性比較複雜。作者暫時沒有弄