在386平台各类保护措施中最重要的就是全局描述符表(GDT)。GDT为内存的某些部分定义了基本的访问权限。咱们可使用GDT中的一个索引来生成段冲突异常, 让内核终止执行异常的进程。现代操做系统大多使用"分页"的内存模式来实现该功能, 它更具通用性和灵活性。GDT还定义了内存中的的某个部分是可执行程序仍是实际的数据。GDT还可定义任务状态段(TSS)。TSS通常在基于硬件的多任务处理中使用, 因此咱们在此并不作讨论。须要注意的是TSS并非启用多任务的惟一方法。数组
注意GRUB已经为你安装了一个GDT, 若是咱们重写了加载GRUB的内存区域, 将会丢弃它的GDT, 这会致使"三重错误(Triple fault)"。简单的说, 它将重置机器。为了防止该问题的发生, 咱们应该在已知能够访问的内存中构建本身的GDT, 并告诉处理器它在哪里, 最后使用咱们的新索引加载处理器的CS、DS、ES、FS和GS寄存器。CS寄存器就是代码段, 它告诉处理器执行当前代码的访问权限在GDT中的偏移量。DS寄存器的做用相似, 可是数据段, 定义了当前数据的访问权限的偏移量。ES、FS和GS是备用的DS寄存器, 对咱们并不重要。函数
GDT自己是64位的长索引列表。这些索引定义了内存中可访问区域的起始位置和大小界限, 以及与该索引关联的访问权限。一般第一个索引, 0号索引被称为NULL描述符。因此咱们不该该将任何的段寄存器设置为0, 不然将致使常见的保护错误, 这也是处理器的保护功能。通用的保护错误和几种异常将在中断服务程序(ISR)那节详细说明。测试
每一个GDT索引还定义了处理器正在运行的当前段是供系统使用的(Ring 0)仍是供应用程序使用的(Ring 3)。也有其余Ring级别, 但并不重要。当今主要的操做系统仅使用Ring 0和Ring 3。任何应用程序在尝试访问系统或Ring 0的数据时都会致使异常, 这种保护是为了防止应用程序致使内核崩溃。GDT的Ring级别用于告诉处理器是否容许其执行特殊的特权指令。具备特权的指令只能在更高的Ring级别上运行。例如"cli"和"sti"禁用和启用中断, 若是应用程序被容许使用这两个指令, 它就能够阻止内核的运行。你将在本教程的后续章节中了解更多有关中断的知识。优化
GDT的描述符组成以下:ui
在咱们的内核教程中, 咱们将建立一个包含3个索引的GDT。一个用于''虚拟''描述符充当处理器内存保护功能的NULL段, 一个用于代码段, 一个用于数据段寄存器。使用汇编操做码lgdt
告诉处理器咱们新的GDT表在哪里。为lgdt
提供一个指向48位的专用的全局描述符表寄存器(GDTR)的指针。该寄存器用来保存全局描述符信息, 0-15位表示GDT的边界位置(数值为表的长度-1), 16-47位存放GDT基地址。而且在咱们访问GDT中不存在偏移的段时, 但愿处理器能够当即建立通常保护错误)。操作系统
咱们可使用3个索引的简单数组来定义GDT。对于咱们的特殊GDTR指针, 咱们只须要声明一个便可。咱们称其为gp
。建立一个新文件gdt.c。在build.bat中添加一行gcc命令来编译gdt.c, 并将gdt.o添加到LD连接文件列表中。下面这些代码组成了gdt.c的前半部分:指针
gdt.ccode
#include <system.h> /* 定义一个GDT索引. __attribute__((packed))用于防止编译器优化对齐 */ 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)); /* GDTR指针 */ struct gdt_ptr { unsigned short limit; unsigned int base; } __attribute__((packed)); /* 声明包含3个索引的GDT和GDTR指针gp */ struct gdt_entry gdt[3]; struct gdt_ptr gp; /* 这是start.asm中的函数, 用来加载新的段寄存器 */ extern void gdt_flush();
gdt_flush()
咱们尚未定义, 该函数使用上面的GDTR指针来告诉处理器新的GDT所在位置, 并从新加载段寄存器, 最后跳转到咱们的新代码段。如今咱们在start.asm的stublet
下的死循环后面添加下面的代码来定义gdt_flush
:blog
start.asm教程
; 这将创建咱们新的段寄存器 ; 经过长跳转来设置CS global _gdt_flush ; 容许C源程序连接该函数 extern _gp ; 声明_gp为外部变量 _gdt_flush: lgdt [_gp] ; 用_gp来加载GDT mov ax, 0x10 ; 0x10是咱们数据段在GDT中的偏移地址 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax jmp 0x08:flush2 ; 0x08是代码段的偏移地址, 长跳转 flush2: ret ; 返回到C程序中
仅为GDT保留内存空间是不够的, 还须要将值写入每一个GDT中, 设置gp
指针, 再调用gdt_flush
进行更新。定义gdt_set_entry()
函数, 该函数使用函数参数的移位给GDT每一个字段设置值。为了让main.c可以使用这些函数, 别忘了将它们添加到system.h中(至少须要把gdt_install
添加进去)。下面为gdt.c的剩下部分:
gdt.c
/* 在全局描述符表中设置描述符 */ void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran) { /* 设置描述符基地址 */ gdt[num].base_low = (base & 0xFFFF); gdt[num].base_middle = (base >> 16) & 0xFF; gdt[num].base_high = (base >> 24) & 0xFF; /* 设置描述符边界 */ gdt[num].limit_low = (limit & 0xFFFF); gdt[num].granularity = ((limit >> 16) & 0x0F); /* 最后,设置粒度和访问标志 */ gdt[num].granularity |= (gran & 0xF0); gdt[num].access = access; } /* 由main函数调用 * 设置GDTR指针, 设置GDT的3个索引条码 * 最后调用汇编中的gdt_flush告诉处理器新GDT的位置 * 并跟新新的段寄存器 */ void gdt_install() { /* 设置GDT指针和边界 */ gp.limit = (sizeof(struct gdt_entry) * 3) - 1; gp.base = &gdt; /* NULL描述符 */ gdt_set_gate(0, 0, 0, 0, 0); /* 第2个索引是咱们的代码段 * 基地址是0, 边界为4GByte, 粒度为4KByte * 使用32位操做数, 是一个代码段描述符 * 对照本教程中GDT的描述符的表格 * 弄清每一个值的含义 */ gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF); /* 第3个索引是数据段 * 与代码段几乎相同 * 但access设置为数据段 */ gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF); /* 清除旧的GDT安装新的GDT */ gdt_flush(); }
如今咱们的GDT加载程序的基本结构已经到位, 在将其编译连接到内核中后, 咱们须要在main.c中调用gdt_install()
才能真正完成工做。在main()
函数的第一行添加gdt_install();
GDT加载必须最早初始化。如今, 编译你的内核, 并在软盘中对其进行测试, 你不会在屏幕上看到任何变化, 这是一个内部的更改。
下面咱们将进入中断描述符表(IDT)!
若是编译的时候报错:
undefined reference to `_gp'
undefined reference to `gdt_flush'
则把start.asm中_gp
和_gdt_flush
前面的下划线去掉再从新编译。