作業系統——記憶體段的保護(七)
作業系統——記憶體段的保護(七)
2020-09-19 09:12:41 hawk
概述
因為前面已經大體介紹了一下GDT中段描述符所設定的記憶體段的屬性位,但是對於其效果和保護檢查還不是很清楚。這裡再次通過實驗進行測試和驗證,方便我們更加深入的理解GDT的作用。
段暫存器中選擇子保護
判斷是否超越GDT的界限
正如前面分析過的,選擇子的高13位是段描述符的索引值,第0~1位是RPL,第2位是TI位。對於RPL和TI位,實際上並不能進行明顯的檢查,因為其只有在執行的時候,才能體現出錯誤,單純的靜態分析無法分析出相關的錯誤。因此可以直接靜態檢查的只有選擇子的索引值。cpu需要保證選擇子的索引值一定要小於等於描述符表的界限,無論是GDT亦或是LDT,後面我們就預設在GDT中。
我們知道,每一個段描述符大小是8位元組,因此實際上選擇子對應的邊界也就是
描述符表基地址 + 選擇子中的索引值 * 8 + 7
而描述符表的邊界可以直接通過相關的暫存器獲取,結果如下所示
描述符表基地址 + 描述符表界限值
因此cpu只要確保選擇子對應的邊界小於等於描述符表的邊界即可,即
選擇子中的索引值 * 8 + 7 <= 描述符表界限值
我們在loader進入保護模式後嘗試修改其原始碼,使其載入超過描述符表界限值(0x1f)的選擇子(不妨就設定為0000000000100_0_00b)。原始碼修改如下所示
PROTECTION_MODE_START: mov ax, GDT_SECT_DATASTACKmov ds, ax mov gs, ax mov ss, ax ;初始化各個段暫存器,將其都指向GDT_SECT_DATASTACK段描述符對應的段 mov esp, LOADER_STACK_TOP mov ax, GDT_SECT_VIDEO mov es, ax ;這裡將es設定為GDT_VIDEO段描述符對應的段,即視訊記憶體段,那裡沒有使用平坦模式,訪問視訊記憶體仍然類似於真實模式 jmp 0000000000100_0_00b:0
結果如下所示
可以看到,我們將程式碼段暫存器中的選擇子的索引值更新為4後,當執行完這條指令後,cpu會根據根據更新的段程式碼暫存器的值去GDT中獲取相關的段描述符,從而cpu會檢測到異常的索引值,最終丟擲錯誤。
判斷是否引用GDT的第0個段描述符
前面也分析過了,為了防止段暫存器沒有被初始化,因此設定GDT中的第0個段描述符不可被訪問(對於LDT則沒有這種限制)。這裡需要說明的是,在保護模式下,CS段暫存器和SS段暫存器是直接不可以載入索引值為0的GDT的選擇子(對於cs段暫存器,因為cpu執行下一條指令會用到cs段暫存器,因此如果更改後會立馬被檢測到;對於ss段暫存器的原因還不是很明白);對於其他段暫存器來說(ds、es、fs、gs段暫存器),雖然可以載入索引值為0的GDT的選擇子,但是在真正使用到這些段暫存器的時候,cpu仍然會丟擲異常,從而導致錯誤。我們首先嚐試修改ss段暫存器的選擇子,原始碼如下所示
[bits 32] PROTECTION_MODE_START: mov ax, 0000000000000_0_00b mov ss, ax
最後執行的結果如圖所示
可以看到,對於ss段暫存器、cs段暫存器等,確實直接不讓裝載索引值為0的選擇子,裝載的話cpu直接會跑出異常。下面我們在嘗試一下es段暫存器,原始碼如下所示
可以看到,對於ds、es、fs和gs段暫存器來說,可以載入索引值為0的選擇子,但是當其真正進行定址的時候,cpu會丟擲異常,仍然相當於無法使用GDT的第0個段描述符。
檢查段型別
我們知道,段描述符中包含TYPE欄位和S欄位,其共同表示段的用途和型別。自然的,當我們在操作的時候,cpu會對這些段的屬性進行檢查,確保符合一些固定的規則,大體規則如下所示
1. 只有具備可執行屬性的段(程式碼段)才能載入到cs段暫存器中 2. 只具備執行屬性的段(程式碼段)不允許載入到除cs外的段暫存器中 3. 只有具備可寫屬性的段(資料段)才能載入到ss棧段暫存器中 4. 至少具備可讀屬性的段才能載入到ds、es、fs、gs段暫存器中
形象化一下上面的規則,如下表所示
段暫存器 | 程式碼段(X=1) | 資料段(X=0) | ||
只執行(R=0) | 執行+可讀(R=1) | 只讀(R=1,W=0) | 讀寫(R=1,W=1) | |
CS | 通過檢查 | 通過檢查 | 不通過檢查 | 不通過檢查 |
DS | 不通過檢查 | 通過檢查 | 通過檢查 | 通過檢查 |
ES | 不通過檢查 | 通過檢查 | 通過檢查 | 通過檢查 |
FS | 不通過檢查 | 通過檢查 | 通過檢查 | 通過檢查 |
GS | 不通過檢查 | 通過檢查 | 通過檢查 | 通過檢查 |
SS | 不通過檢查 | 不通過檢查 | 不通過檢查 | 通過檢查 |
這裡由於規則比較多,我們就選取兩條具有代表性質的規則進行檢驗即可,首先是cpu對於ES段暫存器載入只執行程式碼段的段描述符的檢查(因為之前我一直沒搞懂對於程式碼段來說什麼是可執行,什麼是可讀);其次測試cpu對於ss段暫存器載入不可寫的段描述符的檢查。首先是第一個,我們構造的程式碼段其已經滿足是隻執行程式碼段,即不可讀,我們直接裝載即可,原始碼如下所示
[bits 32] PROTECTION_MODE_START: mov ax, 0000000000001_0_00b mov es, ax
結果如圖所示
可以看到,如果是不可讀的記憶體段的段描述符,確實不能被載入入es段暫存器。下面我們檢測第二個規則。同樣用不可寫的程式碼段段描述進行測試,原始碼如下所示
[bits 32] PROTECTION_MODE_START: mov ax, 0000000000001_0_00b mov ss, ax
結果如圖所示
程式碼段和資料段的保護
實際上,對於程式碼段和資料段來說,除了上面的檢查外,cpu每訪問一個地址,都要確認該地址不能超過其所在的記憶體段的範圍。也就是段描述符中段機制和段界限共同描述的範圍。這裡我們假設其都是預設向上擴充套件的(主要針對的資料段)。之前已經分析過了,實際段界限的值為
(段描述符中段界限 +1) ×(段界限的力度大小:4KB/1B)-1
由於我們實現的GDT中的段描述符基本上其段界限粒度都是4KB的,因此我們可以將上面的公式進行展開,如下所示
實際段界限大小 = 段描述符中界限 * 0x1000 + 0xfff
因此,cpu會在檢查的時候判斷相關資料(純資料/程式碼)最終是否超過了段的邊界,也就是檢查如下等式
偏移地址+資料長度/指令長度 - 1 <= 實際段界限大小
如果不滿足上面的檢查,則CPU就會直接丟擲異常,導致程式執行錯誤。我們這裡就簡單的通過對於前面實現的GDT中的視訊記憶體段(可以相當於資料段)進行驗證,我們前面設定的段描述符的段邊界是0x7,轉換為對應的實際段界限也就是
0x7 * 0x1000 + 0xfff = 0x7fff
那麼我們就訪問該段的偏移地址為0x7fff,資料長度為2的資料即可,原始碼如下所示
[bits 32] PROTECTION_MODE_START: mov ax, GDT_SECT_VIDEO mov es, ax mov byte al, [es:0x7fff] mov word ax, [es:0x7fff]
執行結果如圖所示
可以看到,es對應的段描述符的實際段邊界大小是0x7fff,當我們訪問es:0x7fff的一位元組資料時,程式仍然正常執行;當我們訪問es:0x7fff的2位元組資料時,cpu檢測到了不符合規則,則直接丟擲異常。
棧段的保護
這次我們分析的是以向下擴充套件的資料段作為棧段(前面的實驗我們是以普通的向上擴充套件的資料段作為棧段,其保護和普通的資料段沒有什麼區別),用來了解清楚段描述符中各個屬性的具體作用。這裡我首先想說明清楚一個問題,段描述符中向上擴充套件/向下擴充套件的含義。書上是這樣講的
1. 對於向上擴充套件的段,實際的段界限是段內可以訪問的最後一位元組;
2. 對於向下擴充套件的段,實際的段界限是段內不可以訪問的第一個位元組。
老實說,這裡我是花費了很長時間才弄明白的,這裡我直接更直觀的給出資料和實驗結果,方便大家理解的更透徹。
這是向上擴充套件的段描述符對應的可用記憶體,這個看起來還是比較好理解的,唯一需要注意的是段最大大小,那個是根據段描述符中段界限取最大值和段界限的粒度大小共同得出來的。下面則是向下擴充套件的段描述符,示意圖如下所示
也就是實際上段界限將理論上段的最大值的記憶體空間一份為二,其中向上擴充套件使用的是靠基址部分的;向下擴充套件是另外一部分(由於是32位地址線,如果選擇段界限粒度為4K,我認為會地址迴繞,不過沒有實驗)。對於向上擴充套件的資料段,我們實際上已經在這篇部落格的前半部分進行了驗證;而對於向下擴充套件的資料段,我們講視訊記憶體段的TYPE中E置為1,然後測試程式碼如下所示
[bits 32] PROTECTION_MODE_START: mov ax, GDT_SECT_VIDEO mov es, ax mov byte bl, [es:0x8000] mov byte bl, [es:0x7fff]
結果如圖所示
可以看到,可以正常訪問地址為es:0x8000處的記憶體,但是無法訪問地址為es:0x7fff處的記憶體。既然我們明白了這個道理,實際上對於向下擴充套件的棧段就十分容易進行檢查了。下面我們使用的僅僅都是段內偏移地址,也就是整個可訪問的棧的空間為
[limit + 1, 0xFFFFFFFF/0xFFFFF]
只需要確保棧上所有資料相對基址的偏移處於這個範圍內即可(需要邊界的時候還需要考慮資料本身的大小,而非僅僅考慮資料起始地址)