GDT简介
在intel 8086体系结构中,有6个段寄存器,CPU取址采用段:偏移模式。从80286开始,为描述不同的段结构,x86架构引入了GDT(Global Descriptor Table,全局描述符表)。GDT可以描述程序各段的组成结构,其中主要包含了基地址(base address)、大小(limit)、权限(privilege)等信息。值得一提的是,GDT中还可以包含LDT的描述符。LDT,即Local Descriptor Table(局部描述表),可以用来描述每个程序独立的局部段描述信息。
这种架构的出现,其实和当时操作系统的发展紧密相关。因为操作系统的实现,除了要依靠软件设计,还需要底层硬件对某些特性的支持。我们可以简单地看看GDT的结构,没必要进行深入的理解。因为GDT中某些丑陋的设计是出于对老平台的兼容以及对当时的操作系统的考量,所以我们没有必要将精力放在研究这些遗留问题之上。
万恶的“Backward Campability”,因此我们在实现操作系统时没有办法绕过GDT,因此我们必须对其有个简单的认识。
简要的理论知识(可跳过)
GDT描述符
GDT长度是可变的
因此我们需要告诉底层硬件,我们操作系统中的GDT位置及大小。为此x86体系定义了如下数据结构用以描述GDT信息(包括GDT的大小,以及存放GDT的线性地址)。
对上图需要进行简单的补充:Size为GDT所占的字节大小-1。因为size为两字节,所以GDT最大的大小为65535字节(供8192表项)。
x86体系架构,提供lgdt指令来加载gdt描述符。即lgdt [上图所示的表的地址]
GDT表项
GDT的每个表项,抽象地可以看成包含四个字段的数据结构:基地址(Base),大小(Limit),标志(Flag),访问信息(Access Byte)。
GDT中每个表项在内存中的实际布局如下图所示:
为什么描述段基地址的Base以及段大小的Limit字段会被拆成这种丑陋的结构?
因为需要兼容286架构,啊哈哈哈哈~。
Base和Limit字段很好理解,Flag和Access Byte字段如下图所示:
其中每个字段代表的意义在此不做展开,这里只指出一点:Privl字段描述了对应段的权限等级(Ring 0~3)。
LDT
LDT,Local Descriptor Table,局部描述符表。在分页机制出现以前的操作系统中并没有虚拟内存(Virtual Memory)这个概念。为了让不同程序的数据彼此互不干扰,x86架构引入了LDT概念,期望操作系统可以通过为不同的应用程序设置不同的LDT而隔离程序间的数据。程序在使用分段机制取址的时候,可是通过设置选择子(selector)的特定位而告诉CPU是从GDT还是从LDT中选择对应的段信息,如下图所示。更多的细节在此不做展开。
随着分页机制的提出,GDT所代表的分段机制逐渐废弃。对于现代操作系统而言,GDT的作用几乎只是用来改变当前CPU执行的特权级,并且改变CPU的特权级也只有这种方式。
用代码操作GDT
加载GDT描述符的任务,必须由汇编指令完成,因此我们实现了一个汇编方法_flush_gdt, 并将其暴露给C文件。注释虽然用英文写的,但是我相信大家应该能阅读。汇编代码使用AT&T语法,使用gas作为编译器。
使用汇编代码加载GDT
(完成这段代码花了我一个小时,泪奔~~。因为一个小小的typo导致debug了好久,各位可直接拿去使用)
#function for loading gdt
.global _flush_gdt
.type _flush_gdt, @function
_flush_gdt:
movl 4(%esp), %eax #Get the first argument, which is the pointer to gdt descriptor
lgdt (%eax) #load gdt descriptor
movw $0x10, %ax #0x10 is the offset in the GDT to our data segment
mov %ax, %ds #copy %ax to ds,es,fs,gs,ss
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
mov %ax, %ss
ljmp $0x08,$flush_lable #Using a long jump to set %cs to 0x8
flush_lable:
ret
使用C语言填充GDT表项
由于代码里都有很好的注释,我就不再做过多介绍了。
gdt.h
主要是定义了RING 0和RING 3下数据段和代码段的Flag与Access Byte字段。
#ifndef _KERNEL_GDT_H
#define _KERNEL_GDT_H
#include
//Each define here is for a specific bit(bits) flag for a gdt entry
#define SEG_DESCTYPE(x) ((x) << 0x04) //Descriptor type (0 for system, 1 for code/data)
#define SEG_PRES(x) ((x) << 0x07) // Present
#define SEG_SAVL(x) ((x) << 0x0C) // Available for system use
#define SEG_LONG(x) ((x) << 0x0D) // Long mode
#define SEG_SIZE(x) ((x) << 0x0E) // Size (0 for 16-bit, 1 for 32)
#define SEG_GRAN(x) ((x) << 0x0F) // Granularity (0 for 1B - 1MB, 1 for 4KB - 4GB)
#define SEG_PRIV(x) (((x) & 0x03) << 0x05) // Set privilege level (0 - 3)
#define SEG_DATA_RD 0x00 // Read-Only
#define SEG_DATA_RDA 0x01 // Read-Only, accessed
#define SEG_DATA_RDWR 0x02 // Read/Write
#define SEG_DATA_RDWRA 0x03 // Read/Write, accessed
#define SEG_DATA_RDEXPD 0x04 // Read-Only, expand-down
#define SEG_DATA_RDEXPDA 0x05 // Read-Only, expand-down, accessed
#define SEG_DATA_RDWREXPD 0x06 // Read/Write, expand-down
#define SEG_DATA_RDWREXPDA 0x07 // Read/Write, expand-down, accessed
#define SEG_CODE_EX 0x08 // Execute-Only
#define SEG_CODE_EXA 0x09 // Execute-Only, accessed
#define SEG_CODE_EXRD 0x0A // Execute/Read
#define SEG_CODE_EXRDA 0x0B // Execute/Read, accessed
#define SEG_CODE_EXC 0x0C // Execute-Only, conforming
#define SEG_CODE_EXCA 0x0D // Execute-Only, conforming, accessed
#define SEG_CODE_EXRDC 0x0E // Execute/Read, conforming
#define SEG_CODE_EXRDCA 0x0F // Execute/Read, conforming, accessed
#define GDT_CODE_PL0 (SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | SEG_PRIV(0) | SEG_CODE_EXRD)
#define GDT_DATA_PL0 (SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | SEG_PRIV(0) | SEG_DATA_RDWR)
#define GDT_CODE_PL3 (SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | SEG_PRIV(3) | SEG_CODE_EXRD)
#define GDT_DATA_PL3 (SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | SEG_PRIV(3) | SEG_DATA_RDWR)
struct gdt_descriptor_struct{
uint16_t limit;
uint32_t base;
}
__attribute__((packed));
typedef struct gdt_descriptor_struct gdt_descriptor;
typedef uint64_t gdt_entry;
void init_gdt();
#endif
gdt.c
init_gdt()函数完成了对GDT的设置和加载。主要是设置了ring 0的代码段和数据段(各一个),ring 3的代码段和数据段(各一个)。最开始的空表项是硬件必须的(如果没有这项,bochs会报错)。
#include
gdt_entry entrys[5];
gdt_descriptor gdtd;
extern void _flush_gdt(uint32_t gdtp);
static gdt_entry create_entry(uint32_t base, uint32_t limit, uint16_t flag){
uint64_t entry ;
// Create the high 32 bit segment
entry = limit & 0x000F0000; // set limit bits 19:16
entry |= (flag << 8) & 0x00F0FF00; // set type, p, dpl, s, g, d/b, l and avl fields
entry |= (base >> 16) & 0x000000FF; // set base bits 23:16
entry |= base & 0xFF000000; // set base bits 31:24
// Shift by 32 to allow for low part of segment
entry <<= 32;
// Create the low 32 bit segment
entry |= base << 16; // set base bits 15:0
entry |= limit & 0x0000FFFF; // set limit bits 15:0
return entry;
}
void init_gdt(){
gdtd.limit = (sizeof(gdt_entry) * 5) - 1;
gdtd.base = (uint32_t)entrys;
//Fill data to gdt entries
entrys[0] = create_entry(0, 0, 0); //Needed
entrys[1] = create_entry(0, 0x000FFFFF, (GDT_CODE_PL0)); // Ring 0 code section
entrys[2] = create_entry(0, 0x000FFFFF, (GDT_DATA_PL0)); // Ring 0 data section
entrys[3] = create_entry(0, 0x000FFFFF, (GDT_CODE_PL3)); // Ring 3 code section
entrys[4] = create_entry(0, 0x000FFFFF, (GDT_DATA_PL3)); // Ring 3 data section
_flush_gdt((uint32_t)&gdtd);
}
实际运行结果
如下图所示,使用bochs加载我的操作系统之后,通过自带的debug功能能得到下述的GDT信息,和我们在C代码中设置的一致。Succeed~
参考文献
- https://www.wikiwand.com/en/Global_Descriptor_Table
- http://www.jamesmolloy.co.uk/tutorial_html/4.-The%20GDT%20and%20IDT.html
- http://wiki.osdev.org/GDT_Tutorial
- http://wiki.osdev.org/Global_Descriptor_Table
- http://www.osdever.net/bkerndev/Docs/gdt.htm
- http://bochs.sourceforge.net/doc/docbook/user/internal-debugger.html