從零開始寫 OS 核心 - 全域性描述符表 GDT
系列目錄
- 序篇
- 準備工作
- BIOS 啟動到真實模式
- GDT 與保護模式
- 虛擬記憶體初探
- 載入並進入 kernel
- 顯示與列印
- 全域性描述符表 GDT
- 中斷處理
- 虛擬記憶體完善
- 實現堆和 malloc
- 建立第一個核心執行緒
- 多執行緒執行與切換
- 鎖與多執行緒同步
- 程序的實現
- 進入使用者態
- 一個簡單的檔案系統
- 載入可執行程式
- 系統呼叫的實現
- 鍵盤驅動
- 執行 shell
擴充套件並重載 GDT
本篇我們將在 kernel 中重新定義並擴充套件全域性描述符表 GDT,並再次載入它。本篇的內容也會比較簡單,更多的是對 x86 相關手冊文件的查閱和熟悉。
GDT 在 loader
階段我們已經初步定義並載入過一次,在那裡我們只定義了 kernel 的 code
data
段,因為到目前為止,以及在後面相當長的一段時間裡,我們始終處於 kernel 空間中,以 CPU 特權級 0 進行執行。但是作為一個 OS,最終是要執行並管理使用者程式的,因此 GDT 中還需要加入使用者態的 code
和 data
段。另外我們也希望對前面的 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 項保留;
- 第一個是
kernel
的code segment
; - 第二個是
kernel
的data segment
; - 第三個是 video segment,這個不是必須的,可以無視;
- 第四個是
user
的code segment
; - 第五個是
user
的data 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);
對比 kernel
和 user
部分的差別,主要是兩點:
Access Byte
中的Privl
:一共兩個 bit 位,對kernel
來說它是00
,而對user
則是11
,它的含義是DPL (Descriptor Privilege Level)
,代表的是訪問這個 segment 需要的最小 CPU 特權級。Limit
:因為使用者空間限制在了 3GB 以下,所以它的Limit
是0xBFFFF
,注意Flags
的Gr (Granularity)
位是 1,所以Limit
的 單位是4KB
,可以計算得到(0xBFFFF + 1) * 4KB = 3GB
;
有了這兩點限制,當 CPU 處於使用者態時,它就無法訪問 3GB 以上的 kernel 空間,這樣 segment 機制的作用就發揮出來了。