Bran的內核開發教程(bkerndev)-06 全局描述符表(GDT)

  • 2019 年 10 月 22 日
  • 筆記

全局描述符表(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告訴處理器我們新的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.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.asmstublet下的死循環後面添加下面的代碼來定義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)!