在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的描述符組成以下:ui
在咱們的內核教程中, 咱們將建立一個包含3個索引的GDT。一個用於''虛擬''描述符充當處理器內存保護功能的NULL段, 一個用於代碼段, 一個用於數據段寄存器。使用匯編操做碼lgdt
告訴處理器咱們新的GDT表在哪裏。爲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.ccode
#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
:blog
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)!
若是編譯的時候報錯:
undefined reference to `_gp'
undefined reference to `gdt_flush'
則把start.asm中_gp
和_gdt_flush
前面的下劃線去掉再從新編譯。