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的字段中,均有一些基地址,偏移地址,和一些属性为,他们不是依次排列的,其内存中状态如下图所示。

RayCommand操作系统的实现笔记3--GDT的介绍

在每一个GDT的字段中,有几位说明了他的权限和访问情况。如下图所示。

RayCommand操作系统的实现笔记3--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的设置。

你可能感兴趣的:(command)