GDT是X86上操作系統的一個最基礎的問題。這個文章只在介紹GDT的基本知識。並沒有任何一個RayCommand版本對應這一段東西。因爲實在是太基礎了,我也不想單獨拿這個作爲一個Milestone。但是,下文中介紹的任何實現,均在RayCommand的最新版本中/kernel/driver/x86arch/GDT中,有對應的實現。本文主體翻譯自這裏。但是有一些自己的改變。如果想看原文,請參考英文版。
在Intel X86架構上,有很多保護內存訪問的方法,使得用戶程序禁止訪問內核程序的內存,或者其他程序的內存。其中一個重要的方法是使用全局描述符表(Global Descriptor Table), 也就是GDT。GDT定義了某一段特定內存的權限。我們可以使用GDT中的一個字段,定義某一段內存不能被出了內核程序之外的程序訪問。現代操作系統使用的是"分頁"技術實現這一點。使用分頁技術會具有更大的靈活性。GDT基本上可以說是段式的內存,但是X86平臺中,必須設置GDT,也算是一個歷史遺留的問題。在GDT中還可以設置"任務狀態段"(Task State Segments, Tss)。這個TSS段可以進行硬件的任務切換,但是不再本篇文章的討論範圍中。並且,TSS也不是唯一的多任務的方法。
值得注意的是Grub在加載系統的時候,已經載入了默認的GDT.但是,如果你對GRUB GDT的內存區域進行復寫,會導致GDT的失效,引發一個'triple fault'異常。如果要解決這個問題,我們需要自己建立GDT,並將GDT放到一個可控的不會複寫的內存中。再載入自己的GDT。載入後,再將cs,ds,es等段寄存器,設置成GDT中對應字段的偏移。例如,cs中爲代碼段的偏移。如果GDT中,描述代碼段的內存屬性,位於第0x10處偏移的話,則將cs設置成0x10.(抑或,某些書上翻譯的叫做段選擇子,但我實際上覺得,這個東西只是簡單的偏移而已,說的那麼複雜會讓他人產生困惑)
GDT本身是一個數組。它內部的每一個元素都是一個64位長的字段(原文爲Entry,但是我覺得字段的意思更明確,當然實際上字段應該是Field,Entry應該是入口點)。每一個字段設置了一段內存的屬性,權限等等。一個通常的規範是,GDT的第0個字段,應該是個NULL字段,也就是全爲0的字段。沒有任何一個CS,ES這樣的段寄存器應該設置爲0。由於GDT設置了權限,在越權訪問的情況下,CPU會產生一個"General Protection"異常。
GDT中的每一個字段,同時說明了這段內存運行在什麼狀態下。究竟是運行在內核空間(Ring 0)還是用戶空間(Ring 3)。當然,X86系統還有其他的Ring,但是那些大多數情況都不會用到。在Ring 3中,程序被限制只能執行一些基礎的命令。例如在用戶狀態下,就不可以關中斷。這實際上是對操作系統內核程序的一種保護。
在每一個GDT的字段中,均有一些基地址,偏移地址,和一些屬性爲,他們不是依次排列的,其內存中狀態如下圖所示。
在每一個GDT的字段中,有幾位說明了他的權限和訪問情況。如下圖所示。
上圖中,每一個位代表的意思如下。
- Pr: Present Bit,當前是否在內存中的標誌位。對於任何一個有效的代碼段,都必須是1.
- Privl: Privilege, 特權位,兩位。標誌着這段的Ring等級。最高級爲Ring0(內核狀態),最低級爲Ring3(用戶狀態)。
- Ex: Executable,是否可執行位。如果爲1,則該段內存爲可執行的代碼,即代碼段。否則爲數據段。
-
DC: Direction bit/Conforming bit, 方向或一致性位。
- 如果該段爲代碼段,則位表示方向,如果是0,則該段是向上增長的,反之則是向下增長的。換句話說,向下增長意味着偏移地址要大於基地址。
- 如果該段爲數據段,則該位表示一致性。即地位的代碼段是否能夠方位該數據段。
- RW: Readable/Writeable位。如果該段爲1,則對應代碼段的話爲可讀,對應的數據段可寫。注意的是,代碼段永遠是不可寫的,數據段永遠是可讀的。
- Ac:Accessed bit. 當CPU訪問過的時候,設置這位爲1,當然,初始化的時候,我們需要將這位設置爲0.
- Gr:Granularity bit.粒度位。如果是0,則表示在這個GDT中,任何地址單位都爲Byte。如果是1,則表示其單位爲4KB
- Sz:Operand Size bit. 如果爲0,該段爲16位的段,也就是IP每次會取16位指令。爲1,則爲32位的段。
下面是一些示例代碼,在操作系統中載入三個GDT字段。爲什麼是3個呢?和開始說的一樣,第0個爲NULL的字段,再加上一個數據段一個代碼段正好三段。當我們準備好這三個字段組成的數組後,我們需要一個新的數據結構去加載它,這個數據結構叫做GDT的指針,是個48位的地址,裏面包括GDT的內存地址和GDT的長度。
在GDT.c中,我們定義了GDT的一些數據結構和數據。
/* Defines a GDT entry. We say packed, because it prevents the
* compiler from doing things that it thinks is best: Prevent
* compiler "optimization" by packing */
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));
/* Special pointer which includes the limit: The max bytes
* taken up by the GDT, minus 1. Again, this NEEDS to be packed */
struct gdt_ptr
{
unsigned short limit;
unsigned int base;
} __attribute__((packed));
/* Our GDT, with 3 entries, and finally our special GDT pointer */
struct gdt_entry gdt[3];
struct gdt_ptr gp;
/* This will be a function in start.asm. We use this to properly
* reload the new segment registers */
extern void gdt_flush();
/* Setup a descriptor in the Global Descriptor Table */
void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran)
{
/* Setup the descriptor base address */
gdt[num].base_low = (base & 0xFFFF);
gdt[num].base_middle = (base >> 16) & 0xFF;
gdt[num].base_high = (base >> 24) & 0xFF;
/* Setup the descriptor limits */
gdt[num].limit_low = (limit & 0xFFFF);
gdt[num].granularity = ((limit >> 16) & 0x0F);
/* Finally, set up the granularity and access flags */
gdt[num].granularity |= (gran & 0xF0);
gdt[num].access = access;
}
/* Should be called by main. This will setup the special GDT
* pointer, set up the first 3 entries in our GDT, and then
* finally call gdt_flush() in our assembler file in order
* to tell the processor where the new GDT is and update the
* new segment registers */
void gdt_install()
{
/* Setup the GDT pointer and limit */
gp.limit = (sizeof(struct gdt_entry) * 3) - 1;
gp.base = &gdt;
/* Our NULL descriptor */
gdt_set_gate(0, 0, 0, 0, 0);
/* The second entry is our Code Segment. The base address
* is 0, the limit is 4GBytes, it uses 4KByte granularity,
* uses 32-bit opcodes, and is a Code Segment descriptor.
* Please check the table above in the tutorial in order
* to see exactly what each value means */
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
/* The third entry is our Data Segment. It's EXACTLY the
* same as our code segment, but the descriptor type in
* this entry's access byte says it's a Data Segment */
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);
/* Flush out the old GDT and install the new changes! */
gdt_flush();
}
在上述代碼中,GDTInstall爲安裝GDT的過程,每個gdt_set_gate是設置GDT中每個字段的函數。具體的設置方法是根據GDT
; This will set up our new segment registers. We need to do
; something special in order to set CS. We do what is called a
; far jump. A jump that includes a segment as well as an offset.
; This is declared in C as 'extern void gdt_flush();'
global _gdt_flush ; Allows the C code to link to this
extern _gp ; Says that '_gp' is in another file
_gdt_flush:
lgdt [_gp] ; Load the GDT with our '_gp' which is a special pointer
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 ; Returns back to the C code!
的數據結構設置。最開始設置了一個NULL字段,之後設置的是代碼段,最後設置的是數據段。細心的朋友會發現,GDTFlush並沒有定義,這個函數的目的,是將新設置好的GDT加載給CPU。這段函數使用匯編進行書寫。單獨文件GDT_ASM.S,彙編器爲NASM。代碼如下:
由此,我們加載完了新的GDT,在彙編中,將數據段(0x10,因爲是GDT中第三個字段,每個字段長64bit,也就是長0x08。第三個爲0x08*(3-2) = 0x10 )設置給了ds,es等等。又使用jmp,將代碼段0x08設置給了cs。
現在,只要在Main函數中調用GDTInstall,即可完成GDT的設置。