Bran的核心開發指南(5)

lovehappystudy發表於2007-11-08

全域性描述表(GDT)


  386的各種保護措施的一個重要組成部分是 全域性描述表(Global Descriptor Table),也就是 GDT。GDT為記憶體的特定部份定義基本訪問許可權。我們能使用GDT的一個入口來建立一種程式段異常處理機制:讓核心能終止一個正在執行非法操作的程式。大部分 現代作業系統使用叫做“記憶體分頁”的記憶體管理模式來實現這一點:這可以更靈活而且彈性更高。GDT同時還能定義記憶體中的某個區域是可被執行的,還是資料。GDT也能 定義任務狀態分段(TSSes)。TSS被用在基於硬體的多工處理系統[譯者注:如SMP,對稱多處理器系統]中,我們 就不在這裡討論了。但請記住,TSS並不是實現多工處理的唯一方法。

  你會發現GRUB已經為你安裝了一個GDT,但是如果我們覆蓋了GRUB使用的那部分記憶體,GDT將會失效, 並且會產生被稱為“三鍵錯誤”[譯者注:triple fault,三鍵即熱啟動的那個三個鍵]。簡而言之, 發生錯誤後,計算機會重啟。解決這個問題的方法是在一段我們知道 地址並且可以訪問的記憶體上建立我們自己的GDT。這包括構造 新的GDT, 告訴處理器它在記憶體中的地址,最後用我們 新的入口資料載入CPU的CS、DS、ES、FS 和GS暫存器。CS暫存器就是程式碼段。它將 告訴CPU進入GDT的偏移量。這將獲得執行當前程式碼的許可權。DS暫存器是相似的功能,但它不是針對程式碼,而是針對資料段,它定義的是當前資料的許可權。ES、FS、和GS暫存器只不過是DS暫存器 的替代品,對我們並不重要。

  GDT本身就是一連串的64字長的入口。這些入口定義了允許操作的記憶體區域從哪裡開始,哪裡結束,同時定義了每個入口的許可權。一個通常的規則是GDT的第一個入口,入口0,就是我們知道的NULL描述符。任何暫存器都不能被設定為0,否則將發生一般性保護錯誤(General Protection fault),這是CPU的一項保護特徵。一般性保護錯誤和其它的一些異常將在下一章中斷服務例程(ISR)中進行詳細說明。

  每一個GDT入口同時也定義處理器正在執行的當前片段是系統程式在佔用(Ring 0)還是應用程式在佔用(Ring 3)。當然還有其他型別,但是那些並不重要。當今主要的作業系統只使用Ring 0和Ring 3。有一條基本規則:任何試圖訪問系統或Ring 0資料的應用程式都將會引起異常。這種保護措施用於保護核心免遭應用程式的破壞 。在GDT作用範圍內,Ring等級告訴CPU它是否被允許執行特定的指令。某些指令的許可權很高,意味著它只能在較高的Ring等級上執行。這樣的例子有cli和sti指令。它們分別禁用和啟用中斷。如果一個應用程式被允許使用匯編指令cli或sti,那麼它就可以 有效地使核心停止執行。你將在後面的章節學到更多的中斷。

  每個GDT入口的通道和粒度可以這樣被定義:

  7 6 5 4 3 0

  

  P DPL DT Type

  

  P - 這是當前的段嗎?(1 = Yes)

  DPL - 哪個Ring等級(0 to 3)

  DT - 描述符型別

  Type - 什麼型別?

  

  

  

  7 6 5 4 3 0

  

  G D 0 A Seg Len. 19:16

  

  G - 粒度(0 = 1byte, 1 = 4kbyte)

  D - 運算元大小(0 = 16bit, 1 = 32-bit)

  0 - 永遠是0

  A - 系統可見(總被設定為0)

  

  在這本核心指南里,我們只用3個入口來建立一個GDT。為什麼是3個?我們需要在一開始有一個“啞元”(dummy)描述符來為作為處理器的記憶體保護功能的NULL段。 我們需要為程式碼段(Code Segment)和資料段(Data Segment)暫存器各準備一個入口。我們用匯編操作符lgdt來告訴CPU新的GDT在哪裡。需要給'lgdt'一個指向特殊的48-bit結構的指標 。因為GDT的限制,這個特殊的48-bit結構由16-bits(同樣,當我們使用一個在GDT中不存在的段時,核心需要它來產生一般性保護錯誤)和32-bits(儲存GDT自身的地址)構成。

  我們可以用一個3入口的簡單陣列來定義GDT. 對於這個特殊的GDT指標,我們只需要申明一個。 我們把它叫做gp。建立一個新檔案gdt.c。按指南前部分提到的方法在build.bat中新增內容,以便讓GCC來執行編譯工作。我再一次提醒你:為了建立核心,你需要把gdt.o新增到LD聯結器的檔案列表裡。下面是gdt.c前半部分的原始碼,請仔細分析:

  #include

  

  /* 定義一個GDT入口. 我們稱之為包裝,因為

  * 他阻止編譯器做他認為最好的事:用包裝的方法阻止

  * 編譯器進行所謂的“優化” */

  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));

  

  /* 包括如下界限的特殊指標: GDT開始的最大位元組, 負1.

  * 同樣,這需要被包裝 */

  struct gdt_ptr

  {

  unsigned short limit;

  unsigned int base;

  } __attribute__((packed));

  

  /* 這是GDT, 3個入口, 最後是特殊的GDT指標*/

  struct gdt_entry gdt[3];

  struct gdt_ptr gp;

  

  /* 這是 start.asm的一個方法. 我們用這個方法適當的過載

  * 新的段暫存器 */

  extern void gdt_flush();

  'gdt.c'管理GDT

  你會注意到我們對一個並不存在的函式gdt_flush()加了一條申明. gdt_flush()函式使用一個特殊的指標來告訴CPU新的GDT的位置,正如在上面你所看到的。我們需要重新載入新的段暫存器,並且最後跳轉到新的程式碼段。研究下面程式碼,然後把它新增到start.asm中stublet後的那個無窮迴圈後。

  這會建立一個新的段暫存器. 我們需要做

  一些特別的命令來設定CS. 我們要做的就稱為

  far jump. 一個跳轉像偏移量一樣包括一個段.

  這裡用'extern void gdt_flush();'來申明

  global _gdt_flush ;允許C程式連線

  extern _gp ;表明'_gp'在另一個檔案裡

  _gdt_flush:

  lgdt [_gp] ;用這個特殊的指標'_gp'載如GDT

  mov ax, 0x10 ;0x10 is the offset in the GDT to our data segment

  mov ds, ax

  mov es, ax

  mov fs, ax

  mov gs, ax

  mov ss, ax

  jmp 0x08:flush2 ;0x08 is the offset to our code segment: Far jump!

  flush2:

  ret ;回到C程式!

  這些內容新增到start.asm中

  僅僅在記憶體中為GDT保留空間是不夠的。我們需要向GDT入口裡寫入值,設定“gp”GDT指標,然後呼叫函式gdt_flush()來更新。下面要介紹的是一個特殊的函式gdt_set_entry(),它使用簡單好用的引數來進行所有移位(shift),以將合適的值填充進GDT入口。你必須在system.h中新增這兩個函式的原型(我們至少需要gdt_install),以便我們能在main.c中使用它們。 請仔細分析下面這些程式碼,它們是gdt.c的後半部分。

  /* 在全域性描述表(GDT)中建立一個描述符*/

  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;

  }

  

  /* 這裡需要被主函式呼叫。這裡要建立特殊的GDT

  * 指標, 在GDT裡建立最開始的3個入口, 然後

  * 為了告訴處理器新的GDT在哪並且更新新的段寄

  * 存器,我們需要在彙編檔案裡呼叫gdt_flush()*/

  void gdt_install()

  {

  /* 設立GDT指標和範圍*/

  gp.limit = (sizeof(struct gdt_entry) * 3) - 1;

  gp.base = &gdt;

  

  /* NULL描述符 */

  gdt_set_gate(0, 0, 0, 0, 0);

  

  /* 第二個入口就是我們的程式碼段(Code Segment)。基地址

  * 是0, 大小是4GBytes, 粒度為4KByte,

  * 使用32-bit操作碼,是一個程式碼段描述符。

  * 請檢查本章前面提到的那個表格,以確保每個

  * 變數的意思正確。*/

  gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);

  

  /*第三個入口 是我們的資料段(Data Segment)。它完全和 程式碼段(Code Segment)

  * 相同, 但是這個入口的訪問標識說明這是一個資料段 */

  gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);

  

  /* 把舊的GDT刪除,安裝新的更新! */

  gdt_flush();

  }

  把這些新增到gdt.c。它從事的是一些和GDT相關的骯髒工作!不要忘記在system.h中設定函式原型!

  既然GDT載入器基本構架已準備就緒並且我們已經把它編譯連線進了核心,我們需要呼叫gdt_install()以讓它工作。開啟main.c,然後再main()函式的最開頭新增“gdt_install();”。正如你在本章中所學到的,GDT需要在最開始就被初始化。它是十分重要的。你現在可以編譯連線並將核心弄到軟盤裡來測試了。你不會在螢幕上看到任何變化,因為這是一個內在的變化。接下來,開始學下一章 中斷描述符表(IDT)吧!



本文轉自

http://rammaker.cosoft.org.cn/store/bkerndev_zh_CN/Docs/gdt.htm

相關文章