Bran的核心開發教程(bkerndev)-06 全域性描述符表(GDT)
全域性描述符表(GDT)
在386平臺各種保護措施中最重要的就是全域性描述符表(GDT)。GDT為記憶體的某些部分定義了基本的訪問許可權。我們可以使用GDT中的一個索引來生成段衝突異常, 讓核心終止執行異常的程序。現代作業系統大多使用"分頁"的記憶體模式來實現該功能, 它更具通用性和靈活性。GDT還定義了記憶體中的的某個部分是可執行程式還是實際的資料。GDT還可定義任務狀態段(TSS)。TSS一般在基於硬體的多工處理中使用, 所以我們在此並不做討論。需要注意的是TSS並不是啟用多工的唯一方法。
注意GRUB已經為你安裝了一個GDT, 如果我們重寫了載入GRUB的記憶體區域, 將會丟棄它的GDT, 這會導致"三重錯誤(Triple fault)"。簡單的說, 它將重置機器。為了防止該問題的發生, 我們應該在已知可以訪問的記憶體中構建自己的GDT, 並告訴處理器它在哪裡, 最後使用我們的新索引載入處理器的CS、DS、ES、FS和GS暫存器。CS暫存器就是程式碼段, 它告訴處理器執行當前程式碼的訪問許可權在GDT中的偏移量。DS暫存器的作用類似, 但是資料段, 定義了當前資料的訪問許可權的偏移量。ES、FS和GS是備用的DS暫存器, 對我們並不重要。
GDT本身是64位的長索引列表。這些索引定義了記憶體中可訪問區域的起始位置和大小界限, 以及與該索引關聯的訪問許可權。通常第一個索引, 0號索引被稱為NULL描述符。所以我們不應該將任何的段暫存器設定為0, 否則將導致常見的保護錯誤, 這也是處理器的保護功能。通用的保護錯誤和幾種異常將在中斷服務程式(ISR)那節詳細說明。
每個GDT索引還定義了處理器正在執行的當前段是供系統使用的(Ring 0)還是供應用程式使用的(Ring 3)。也有其他Ring級別, 但並不重要。當今主要的作業系統僅使用Ring 0和Ring 3。任何應用程式在嘗試訪問系統或Ring 0的資料時都會導致異常, 這種保護是為了防止應用程式導致核心崩潰。GDT的Ring級別用於告訴處理器是否允許其執行特殊的特權指令。具有特權的指令只能在更高的Ring級別上執行。例如"cli"和"sti"禁用和啟用中斷, 如果應用程式被允許使用這兩個指令, 它就可以阻止核心的執行。你將在本教程的後續章節中瞭解更多有關中斷的知識。
GDT的描述符組成如下:
- G: 段界限粒度(Granularity)
- G = 0: 長度單位為1位元組
- G = 1: 長度單位為4KB
- D: 運算元大小
- 0 = 16bit
- 1 = 32bit
- L: 未使用為0
- AVL: 保留位, 系統軟體使用
- P: 存在位, 段是否存在
- 1 = Yes
- 0 = No
- DPL: Ring級別(0到3)
- S: 描述符型別位
- S = 1: 儲存段描述符, 資料段/程式碼段
- S = 0: 系統段描述符/門描述符)
- TYPE: 段型別
在我們的核心教程中, 我們將建立一個包含3個索引的GDT。一個用於''虛擬''描述符充當處理器記憶體保護功能的NULL段, 一個用於程式碼段, 一個用於資料段暫存器。使用匯編操作碼lgdt
lgdt
提供一個指向48位的專用的全域性描述符表暫存器(GDTR)的指標。該暫存器用來儲存全域性描述符資訊, 0-15位表示GDT的邊界位置(數值為表的長度-1), 16-47位存放GDT基地址。並且在我們訪問GDT中不存在偏移的段時, 希望處理器可以立即建立一般保護錯誤)。
我們可以使用3個索引的簡單陣列來定義GDT。對於我們的特殊GDTR指標, 我們只需要宣告一個即可。我們稱其為gp
。建立一個新檔案gdt.c。在build.bat中新增一行gcc命令來編譯gdt.c, 並將gdt.o新增到LD連結檔案列表中。下面這些程式碼組成了gdt.c的前半部分:
gdt.c
#include <system.h>
/* 定義一個GDT索引. __attribute__((packed))用於防止編譯器優化對齊 */
struct gdt_entry
{
unsigned short limit_low;
unsigned short base_low;
unsigned char base_middle;
unsigned char access;
unsigned char granularity;
unsigned char base_high;
} __attribute__((packed));
/* GDTR指標 */
struct gdt_ptr
{
unsigned short limit;
unsigned int base;
} __attribute__((packed));
/* 宣告包含3個索引的GDT和GDTR指標gp */
struct gdt_entry gdt[3];
struct gdt_ptr gp;
/* 這是start.asm中的函式, 用來載入新的段暫存器 */
extern void gdt_flush();
gdt_flush()
我們還沒有定義, 該函式使用上面的GDTR指標來告訴處理器新的GDT所在位置, 並重新載入段暫存器, 最後跳轉到我們的新程式碼段。現在我們在start.asm的stublet
下的死迴圈後面新增下面的程式碼來定義gdt_flush
:
start.asm
; 這將建立我們新的段暫存器
; 通過長跳轉來設定CS
global _gdt_flush ; 允許C源程式連結該函式
extern _gp ; 宣告_gp為外部變數
_gdt_flush:
lgdt [_gp] ; 用_gp來載入GDT
mov ax, 0x10 ; 0x10是我們資料段在GDT中的偏移地址
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
jmp 0x08:flush2 ; 0x08是程式碼段的偏移地址, 長跳轉
flush2:
ret ; 返回到C程式中
僅為GDT保留記憶體空間是不夠的, 還需要將值寫入每個GDT中, 設定gp
指標, 再呼叫gdt_flush
進行更新。定義gdt_set_entry()
函式, 該函式使用函式引數的移位給GDT每個欄位設定值。為了讓main.c能夠使用這些函式, 別忘了將它們新增到system.h中(至少需要把gdt_install
新增進去)。下面為gdt.c的剩下部分:
gdt.c
/* 在全域性描述符表中設定描述符 */
void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran)
{
/* 設定描述符基地址 */
gdt[num].base_low = (base & 0xFFFF);
gdt[num].base_middle = (base >> 16) & 0xFF;
gdt[num].base_high = (base >> 24) & 0xFF;
/* 設定描述符邊界 */
gdt[num].limit_low = (limit & 0xFFFF);
gdt[num].granularity = ((limit >> 16) & 0x0F);
/* 最後,設定粒度和訪問標誌 */
gdt[num].granularity |= (gran & 0xF0);
gdt[num].access = access;
}
/* 由main函式呼叫
* 設定GDTR指標, 設定GDT的3個索引條碼
* 最後調用匯編中的gdt_flush告訴處理器新GDT的位置
* 並跟新新的段暫存器 */
void gdt_install()
{
/* 設定GDT指標和邊界 */
gp.limit = (sizeof(struct gdt_entry) * 3) - 1;
gp.base = &gdt;
/* NULL描述符 */
gdt_set_gate(0, 0, 0, 0, 0);
/* 第2個索引是我們的程式碼段
* 基地址是0, 邊界為4GByte, 粒度為4KByte
* 使用32位運算元, 是一個程式碼段描述符
* 對照本教程中GDT的描述符的表格
* 弄清每個值的含義 */
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
/* 第3個索引是資料段
* 與程式碼段幾乎相同
* 但access設定為資料段 */
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);
/* 清除舊的GDT安裝新的GDT */
gdt_flush();
}
現在我們的GDT載入程式的基本結構已經到位, 在將其編譯連結到核心中後, 我們需要在main.c中呼叫gdt_install()
才能真正完成工作。在main()
函式的第一行新增gdt_install();
GDT載入必須最先初始化。現在, 編譯你的核心, 並在軟盤中對其進行測試, 你不會在螢幕上看到任何變化, 這是一個內部的更改。
下面我們將進入中斷描述符表(IDT)!
PS
如果編譯的時候報錯:
undefined reference to `_gp'
undefined reference to `gdt_flush'
則把start.asm中_gp
和_gdt_flush
前面的下劃線去掉再重新編譯