寫在前面
添油加醋系列第二彈——剖析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):
同樣也是這樣的(Global Descriptor Table):
在代碼中它又是這樣:
// 全局描述符表結構 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就知道了操作系統中段的設置,從而可以通過段選擇子得到線性地址,在后面實現分頁管理后,可進一步將線性地址轉換為物理地址(不過當前連物理 址有多大都沒法知道呢,在后面會解決)。
段選擇子
(GDT 與 LDT - hicjiajia - 博客園)介紹:
段選擇子包括三部分:描述符索引(index)、TI(指示從GDT還是LDT中找)、請求特權級(RPL)。
- index部分表示所需要的段的描述符在描述符表的位置,由這個位置再根據在GDTR中存儲的描述符表基址就可以找到相應的描述符gdt_entry。然后用描述符gdt_entry中的段基址SEL加上邏輯地址OFFSET就可以轉換成線性地址SEL:OFFSET(看下面給的例子應該就是它們的和SEL+OFFSET)
- 段選擇子中的TI值只有一位0或1,0代表選擇子是在GDT選擇,1代表選擇子是在LDT選擇。
- 請求特權級(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的內容很雜很多,所以也只能挑一些重點的講講了,不可能面面俱到,在后面的編寫/借鑒中,還是要以查資料為主,給源碼附上參考文章的地址,方便閱讀。