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的字段中,有几位说明了他的权限和访问情况。如下图所示。
上图中,每一个位代表的意思如下。
下面是一些示例代码,在操作系统中载入三个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!
由此,我们加载完了新的GDT,在汇编中,将数据段(0x10,因为是GDT中第三个字段,每个字段长64bit,也就是长0x08。第三个为0x08*(3-2) = 0x10 )设置给了ds,es等等。又使用jmp,将代码段0x08设置给了cs。
现在,只要在Main函数中调用GDTInstall,即可完成GDT的设置。