RayCommand操作系統的實現筆記3--GDT的介紹

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的設置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章