1. 程式人生 > 其它 >從零開始寫 OS 核心 - 全域性描述符表 GDT

從零開始寫 OS 核心 - 全域性描述符表 GDT

系列目錄

  • 序篇
  • 準備工作
  • BIOS 啟動到真實模式
  • GDT 與保護模式
  • 虛擬記憶體初探
  • 載入並進入 kernel
  • 顯示與列印
  • 全域性描述符表 GDT
  • 中斷處理
  • 虛擬記憶體完善
  • 實現堆和 malloc
  • 建立第一個核心執行緒
  • 多執行緒執行與切換
  • 鎖與多執行緒同步
  • 程序的實現
  • 進入使用者態
  • 一個簡單的檔案系統
  • 載入可執行程式
  • 系統呼叫的實現
  • 鍵盤驅動
  • 執行 shell

擴充套件並重載 GDT

本篇我們將在 kernel 中重新定義並擴充套件全域性描述符表 GDT,並再次載入它。本篇的內容也會比較簡單,更多的是對 x86 相關手冊文件的查閱和熟悉。

GDT 在 loader 階段我們已經初步定義並載入過一次,在那裡我們只定義了 kernel 的 code

data 段,因為到目前為止,以及在後面相當長的一段時間裡,我們始終處於 kernel 空間中,以 CPU 特權級 0 進行執行。但是作為一個 OS,最終是要執行並管理使用者程式的,因此 GDT 中還需要加入使用者態的 codedata 段。

另外我們也希望對前面的 GDT 重新整理一下,畢竟在彙編下比較混亂,很多資料結構管理起來不清晰。

GDT 程式碼

GDT 以及 segment 相關的知識,是 x86 體系架構的歷史遺留產物,非常令人討厭。但是 Intel 為了歷史相容,又不得不始終保留這些歷史包袱。我們也不必花太多心思和腦筋在這上面,只要按照文件規範,把該填都填了,該寫的都寫了,輕輕帶過就可以了。它並不是我們專案的核心部分。

按慣例,先給出程式碼連結,主要原始檔是 src/mem/gdt.c。

關於 GDT 的文件,你可以參考這裡。

首先我們需要定義 GDT entry 的資料結構:

struct gdt_entry {
  uint16 limit_low;
  uint16 base_low;
  uint8  base_middle;
  uint8  access;
  uint8  attributes;
  uint8  base_high;
} __attribute__((packed));
typedef struct gdt_entry gdt_entry_t;

它對應的是這樣一個 64 bit 的結構:

其中 base 是指 segment 的記憶體基址,limit 則是長度,它可以有 1 或者 4KB 兩種單位。

其餘部分則是圖二中展示的一些標誌位元位,這裡就不多費筆墨了,還是要對著文件仔細校對。

然後我們定義 GDT 表:

static gdt_entry_t gdt_entries[7];

我們這裡分配了 7 個 entry:

  • 第 0 項保留;
  • 第一個是 kernelcode segment
  • 第二個是 kerneldata segment
  • 第三個是 video segment,這個不是必須的,可以無視;
  • 第四個是 usercode segment
  • 第五個是 userdata segment
  • 第六個是 tss

從第四個開始,都是使用者態需要用到的。其中第六個 tss 目前不必深究,後面進入使用者態時我們會回過來再細看這部分。

然後我們定義設定 GDT entry 的函式:

static void gdt_set_gate(
    int32 num, uint32 base, uint32 limit, uint8 access, uint8 flags) {
  gdt_entries[num].limit_low = (limit & 0xFFFF);
  gdt_entries[num].base_low = (base & 0xFFFF);
  gdt_entries[num].base_middle = (base >> 16) & 0xFF;
  gdt_entries[num].access = access;
  gdt_entries[num].attributes = (limit >> 16) & 0x0F;
  gdt_entries[num].attributes |= ((flags << 4) & 0xF0);
  gdt_entries[num].base_high = (base >> 24) & 0xFF;
}

對照著上面那幅圖看就可以了。

將 GDT 表中的這些 entry 都設定上:

  // kernel code
  gdt_set_gate(1, 0, 0xFFFFF, DESC_P | DESC_DPL_0 | DESC_S_CODE | DESC_TYPE_CODE, FLAG_G_4K | FLAG_D_32);
  // kernel data
  gdt_set_gate(2, 0, 0xFFFFF, DESC_P | DESC_DPL_0 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);
  // video: only 8 pages
  gdt_set_gate(3, 0, 7, DESC_P | DESC_DPL_0 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);

  // user code
  gdt_set_gate(4, 0, 0xBFFFF, DESC_P | DESC_DPL_3 | DESC_S_CODE | DESC_TYPE_CODE, FLAG_G_4K | FLAG_D_32);
  // user data
  gdt_set_gate(5, 0, 0xBFFFF, DESC_P | DESC_DPL_3 | DESC_S_DATA | DESC_TYPE_DATA, FLAG_G_4K | FLAG_D_32);

對比 kerneluser 部分的差別,主要是兩點:

  • Access Byte 中的 Privl:一共兩個 bit 位,對 kernel 來說它是 00,而對 user 則是 11,它的含義是 DPL (Descriptor Privilege Level),代表的是訪問這個 segment 需要的最小 CPU 特權級。

  • Limit:因為使用者空間限制在了 3GB 以下,所以它的 Limit0xBFFFF,注意 FlagsGr (Granularity) 位是 1,所以 Limit 的 單位是 4KB,可以計算得到 (0xBFFFF + 1) * 4KB = 3GB

有了這兩點限制,當 CPU 處於使用者態時,它就無法訪問 3GB 以上的 kernel 空間,這樣 segment 機制的作用就發揮出來了。