全局描述符表GDT


v2-1089bf810224a8e1ac3e6b9c5d674e44_1200x500

寫在前面

添油加醋系列第二彈——剖析GDT

頭文件:https://github.com/bajdcc/MiniOS/blob/master/include/gdt.h

實現:https://github.com/bajdcc/MiniOS/blob/master/src/kernel/gdt.c

話說C語言的話除了刷刷OJ外,就是用來實現操作系統這個大頭了。C語言比C++少了很多很多臃腫的語法特性,寫起來非常優美(至少寫操作系統是這樣的)。雖說C++有許多的奇技淫巧,一個算法有N種實現方法,但這會讓選擇恐懼症患者(比如我)難堪,比如說一個類要怎樣寫啊等等,,拋開其他不談,假如一個語言的語法特性越少,學起來可能越簡單(剛試過lua語法很簡單)。OK廢話不多說,進入本章主題(涉及OS的資料很雜很偏,如有錯誤望海涵)。

GDT的構成

這個網址不錯(英文的):Global Descriptor Table

首先,根據網上資料,GDT(全局描述符表)又叫段描述符表,暫且就這樣認為吧,如有異議可以提出來。

一個GDT可能是這樣的(GDT與LDT - Lan'Sir - 博客頻道 - CSDN.NET):

v2-6d7ed4310b1767e534fb1c8b69ca276d_hd

同樣也是這樣的(Global Descriptor Table):

v2-7d368769d159149aa35949405a91d08d_hd

在代碼中它又是這樣:

// 全局描述符表結構 http://www.cnblogs.com/hicjiajia/archive/2012/05/25/2518684.html
// base: 基址(注意,base的byte是分散開的)
// limit: 尋址最大范圍 tells the maximum addressable unit
// flags: 標志位 見上面的AC_AC等
// access: 訪問權限
struct gdt_entry {
    uint16_t limit_low;
    uint16_t base_low;
    uint8_t base_middle;
    uint8_t access;
    unsigned limit_high: 4;
    unsigned flags: 4;
    uint8_t base_high;
} __attribute__((packed));

這時你的內心OS:

答案是——它們都是GDT。。

關於C語言的問題:首先,可能有些童鞋不知道struct里那些冒號是神馬意思。(C語言 struct結構體的變量聲明加冒號)這里叫作“位域”,就是占幾個二進制位。同時,它又涉及內存對齊的概念(C語言 結構體的內存對齊問題與位域)。涉及__attribute__((packed))的概念(__attribute__ 你知多少?)它是手動設置對齊大小。

眾所周知,一個字節byte是八個bit,那么結構體中有兩個4bit的成員,不可能用16bit去容納它們吧~讓它們互相擠擠,節省空間,何樂而不為。

可能看到這里,已經花了好多時間了……沒辦法,OS的內容非常多,同時GCC的一些怪異偏僻用法又不得不去領會,所以只能一步步來,慢慢理解,急不得。

至於GDT為什么這樣描述呢,我自創行不行?一個字——標准,你想改,可能你電腦里的硬件設施不答應……

GDT的存在意義

GDT 與 LDT - hicjiajia - 博客園)描述得很清楚。

全局描述符表GDT(Global Descriptor Table)在整個系統中,全局描述符表GDT 只有一張(一個處理器對應一個GDT),GDT可以被放在內存的 任何位置,但CPU必須知道GDT的 入口,也就是 基地址放在哪里,Intel的設計者門提供了一個 寄存器GDTR用來存放GDT的入口地址,程序員將GDT設定在內存中某個位置之后,可以通過 LGDT指令將GDT的入口地址裝入此寄存器,從此以后,CPU就根據此寄存器中的內容作為GDT的入口來訪問GDT了。GDTR中存放的是GDT在內存中的 基地址和其表長界限
也就是說,GDT是全局的,存放在內存中的某個位置,而這個位置是由你來指定給CPU的,換句話說,你來欽定!

設置GDT

現在知道了GDT的struct構成(就是一個個數組元素),那么我們要給CPU的就是一個gdt_entry數組地址啦~

那么設置gdt_entry的方法如下:

void gdt_install(uint8_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t flags) {

    /* 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].limit_high = ((limit >> 16) & 0x0f);

    /* Finally, set up the granularity and access flags */
    gdt[num].flags = flags;

    access |= AC_RE; // 設置保留位為1
    gdt[num].access = access;
}


通過實例認識它:
// 宏定義

#define AC_AC 0x1       // 可訪問 access
#define AC_RW 0x2       // [代碼]可讀;[數據]可寫 readable for code selector & writeable for data selector
#define AC_DC 0x4       // 方向位 direction
#define AC_EX 0x8       // 可執行 executable, code segment
#define AC_RE 0x10      // 保留位 reserve
#define AC_PR 0x80      // 有效位 persent in memory

// 特權位: 01100000b
#define AC_DPL_KERN 0x0  // RING 0 kernel level
#define AC_DPL_USER 0x60 // RING 3 user level

#define GDT_GR  0x8     // 頁面粒度 page granularity, limit in 4k blocks
#define GDT_SZ  0x4     // 大小位 size bt, 32 bit protect mode

// gdt selector 選擇子
#define SEL_KCODE   0x1 // 內核代碼段
#define SEL_KDATA   0x2 // 內核數據段
#define SEL_UCODE   0x3 // 用戶代碼段
#define SEL_UDATA   0x4 // 用戶數據段
#define SEL_TSS     0x5 // 任務狀態段 task state segment http://wiki.osdev.org/TSS

// RPL 請求特權等級 request privilege level
#define RPL_KERN    0x0
#define RPL_USER    0x3

// CPL 當前特權等級 current privilege level
#define CPL_KERN    0x0
#define CPL_USER    0x3

========================================================

/* Setup the GDT pointer and limit */
gp.limit = (sizeof(struct gdt_entry) * NGDT) - 1;
gp.base = (uint32_t)&gdt;

/* null descriptor */
gdt_install(0, 0, 0, 0, 0);
/* kernel code segment type: code addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_KCODE, 0, 0xfffff, AC_RW|AC_EX|AC_DPL_KERN|AC_PR, GDT_GR|GDT_SZ);
/* kernel data segment type: data addr: 0 limit: 4G gran: 4KB sz: bit 32bit */
gdt_install(SEL_KDATA, 0, 0xfffff, AC_RW|AC_DPL_KERN|AC_PR, GDT_GR|GDT_SZ);
/* user code segment type: code addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_UCODE, 0, 0xfffff, AC_RW|AC_EX|AC_DPL_USER|AC_PR, GDT_GR|GDT_SZ);
/* user code segment type: data addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_UDATA, 0, 0xfffff, AC_RW|AC_DPL_USER|AC_PR, GDT_GR|GDT_SZ);

我的理解是,gdt_install的參數:(段選擇子索引號/見題圖,基址起始,長度,訪問權限,GDT flags)。雖然上述例子中基址起始地址和長度都是一樣的(原項目https://github.com/SilverRainZ/OS677是這樣寫的,可能有點問題),但是訪問權限中有AC_EX和AC_DPL_KERN(ring0)/AC_DPL_USER(ring3)的變化,說明每個段的權限是不同的。這些段管理的是同一片內存,只是由於當前索引號的不同,訪問/修改內存的權限也不同。

GDT 與 LDT - hicjiajia - 博客園)講述了分段管理和分頁管理:

分段管理可以把虛擬地址轉換成線性地址,而分頁管理可以進一步將線性地址轉換成物理地址。

(根據段選擇子找到)段基指 + 偏移地址 => 線性地址

線性地址 (通過頁表) => 物理地址

通過將GDT告訴給CPU后,CPU就知道了操作系統中段的設置,從而可以通過段選擇子得到線性地址,在后面實現分頁管理后,可進一步將線性地址轉換為物理地址(不過當前連物理 址有多大都沒法知道呢,在后面會解決)。

段選擇子

v2-c9ea9406faa3314831f527ef6a0a8d29_hd

GDT 與 LDT - hicjiajia - 博客園)介紹:

段選擇子包括三部分:描述符索引(index)、TI(指示從GDT還是LDT中找)、請求特權級(RPL)。

  1. index部分表示所需要的段的描述符在描述符表的位置,由這個位置再根據在GDTR中存儲的描述符表基址就可以找到相應的描述符gdt_entry。然后用描述符gdt_entry中的段基址SEL加上邏輯地址OFFSET就可以轉換成線性地址SEL:OFFSET(看下面給的例子應該就是它們的和SEL+OFFSET)
  2. 段選擇子中的TI值只有一位0或1,0代表選擇子是在GDT選擇,1代表選擇子是在LDT選擇。
  3. 請求特權級(RPL)則代表選擇子的特權級,共有4個特權級(0級、1級、2級、3級),0級最高。關於特權級的說明:任務中的每一個段都有一個特定的級別。每當一個程序試圖訪問某一個段時,就將該程序所擁有的特權級與要訪問的特權級進行比較,以決定能否訪問該段。系統約定,CPU只能訪問同一特權級或級別較低特權級的段

例如:

給出邏輯地址:21h:12345678h,需要將其轉換為線性地址
a. 選擇子SEL=21h=0000000000100 0 01b,他代表的意思是:選擇子的index=4即100b,選擇GDT中的第4個描述符;TI=0代表選擇子是在GDT選擇;左后的01b代表特權級RPL=1(因此有SEL=n<<3,n是索引號)
b. OFFSET=12345678h,若此時GDT第四個描述符中描述的段基址(Base)為11111111h,則線性地址=11111111h+12345678h=23456789h

任務狀態段TSS

任務寄存器(TR)用於尋址一個特殊的任務狀態段(Task State Segment,TSS)。TSS中包含着當前執行任務的重要信息。

TR寄存器用於存放當前任務TSS段的16位段選擇符、32位基地址、16位段長度和描述符屬性值。它引用GDT表中的一個TSS類型的描述符。指令LTR和STR分別用於加載和保存TR寄存器的段選擇符部分。當使用LTR指令把選擇符加載進任務寄存器時,TSS描述符中的段基地址、段限長度以及描述符屬性會被自動加載到任務寄存器中。當執行任務切換時,處理器會把新任務的TSS的段選擇符和段描述符自動加載進任務寄存器TR中。

它的初始化和設置:

void tss_init() {
    gdt_install(SEL_TSS, (uint32_t)&tss, sizeof(tss),AC_PR|AC_AC|AC_EX, GDT_GR);
    /* for tss, access_reverse bit is 1 */
    gdt[5].access &= ~AC_RE;
}

// 裝載TSS
void tss_install() {
    __asm__ volatile("ltr %%ax" : : "a"((SEL_TSS << 3)));
}

// 設置TSS
void tss_set(uint16_t ss0, uint32_t esp0) {
    // 清空TSS
    memset((void *)&tss, 0, sizeof(tss));
    tss.ss0 = ss0;
    tss.esp0 = esp0;
    tss.iopb_off = sizeof(tss);
}

跟GDT也差不了多少,只是GDT_SZ沒有了,也指定了tss的地址,並設置gdt_entry的保留位為1(至於為啥我沒有仔細查)。至於__asm__ volatile的GCC在C語言中內嵌匯編 asm __volatile__我也沒全部搞明白怎么用。SEL_TSS << 3的話要參考選擇子的構成,它高13位是索引,所以要乘8。

關於ltr指令(設置TSS結構中堆棧信息的 ltr 指令):

在任務內發生特權級變換時堆棧也隨着自動切換,外層堆棧指針保存在內層堆棧中,而內層堆棧指針存放在當前任務的TSS中。所以,在從外層向內層變換時,要訪問TSS(從內層向外層轉移時不需要訪問TSS,而只需訪問內層棧中保存的棧指針)。
LTR指令是專門用於裝載任務狀態段寄存器TR的指令。該指令的操作數是對應TSS段描述符的選擇子。LTR指令從GDT中取出相應的TSS段描述符,把TSS段描述符的基地址和界限等信息裝入TR的高速緩沖寄存器中。

TSS的構成在https://github.com/bajdcc/MiniOS/blob/master/include/idt.h中(看下面的英文注釋/Task State Segment,就是說SS0、ESP0比較重要)。

// 任務狀態段 task state segment http://wiki.osdev.org/TSS
// The only interesting fields are SS0 and ESP0.
//   SS0 gets the kernel datasegment descriptor (e.g. 0x10 if the third entry in your GDT describes your kernel's data)
//   ESP0 gets the value the stack-pointer shall get at a system call
//   IOPB may get the value sizeof(TSS) (which is 104) if you don't plan to use this io-bitmap further (according to mystran in http://forum.osdev.org/viewtopic.php?t=13678)

// http://blog.csdn.net/huybin_wang/article/details/2161886
// TSS的使用是為了解決調用門中特權級變換時堆棧發生的變化

// http://www.kancloud.cn/wizardforcel/intel-80386-ref-manual/123838
/*
    TSS 狀態段由兩部分組成:
    1、 動態部分(處理器在每次任務切換時會設置這些字段值)
        通用寄存器(EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI)
        段寄存器(ES,CS,SS,DS,FS,GS)
        狀態寄存器(EFLAGS)
        指令指針(EIP)
        前一個執行的任務的TSS段的選擇子(只有當要返回時才更新)
    2、 靜態字段(處理器讀取,但從不更改)
        任務的LDT選擇子
        頁目錄基址寄存器(PDBR)(當啟用分頁時,只讀)
        內層堆棧指針,特權級0-2
        T-位,指示了處理器在任務切換時是否引發一個調試異常
        I/O 位圖基址
*/
struct tss_entry {
    uint32_t link;
    uint32_t esp0;
    uint32_t ss0;
    uint32_t esp1;
    uint32_t ss1;
    uint32_t esp2;
    uint32_t ss2;
    uint32_t cr3;
    uint32_t eip;
    uint32_t eflags;
    uint32_t eax;
    uint32_t ecx;
    uint32_t edx;
    uint32_t ebx;
    uint32_t esp;
    uint32_t ebp;
    uint32_t esi;
    uint32_t edi;
    uint32_t es;
    uint32_t cs;
    uint32_t ss;
    uint32_t ds;
    uint32_t fs;
    uint32_t gs;
    uint32_t ldtr;
    uint16_t padding1;
    uint16_t iopb_off;
} __attribute__ ((packed));

階段性總結

涉及OS的內容真是龐大,單單一個GDT就涉及巨量的知識,包括結構體定義、匯編指令、GCC黑魔法、參數的使用等,還涉及了TSS,目標僅僅是實現分段管理。而后面還有中斷管理、物理內存管理、虛擬內存管理等一系列內容,篇幅絕對不比本文少,真令人望洋興嘆。

原始項目OS67中也存在着一些錯誤,有些錯誤像是單詞拼寫等我已經糾正了,還有些如軟盤訪問我去參考了網上的資料,與OS67的不一致,但我沒采用OS67的。畢竟OS67也是其作者自己摸索出來的,讓我跳過了許多坑。。不過我想后面的進程管理還是得自己寫才能體會更深。

既然OS的內容很雜很多,所以也只能挑一些重點的講講了,不可能面面俱到,在后面的編寫/借鑒中,還是要以查資料為主,給源碼附上參考文章的地址,方便閱讀。

https://zhuanlan.zhihu.com/p/25867829備份。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM