[翻译]Global Descriptor Table-GDT

全局描述符表(GDT)是Intel x86处理器用于划分内存区域和控制访问权限的数据结构。GDT包含段信息,如任务状态描述子、本地描述符表描述子等。每个描述子长度为8字节,程序通过段选择子访问。64位系统下,GDT依然存在但扩展到80位。GDT在内存管理和多任务处理中起到关键作用,如定义代码和数据段的权限,但现代操作系统更多依赖分页机制。GDT的修改需重新加载到寄存器才生效,否则可能导致系统错误或重启。
摘要由CSDN通过智能技术生成

全局描述符表(GDT)是 Intel x86 系列处理器(从 80286 开始)所使用的一种数据结构,目的是为了在程序运行期间划分具有不同属性的内存区域,比如:可以运行、可写入等区域的起始地址与访问权限。这些区域被称作段。

全局描述符表除了可以保存段信息外还可以保存其它信息。全局描述符表中的每个表项(描述子)长度为 8-byte,全局描述符表的选择子可以为:任务状态描述子(TSS)、本地描述符表描述子或者调用门描述子。调用门在 x86 不同特权级中转移控制权非常重要,但是现在的操作系统很少使用这种机制。

同时存在的还有局部描述符表(LDT)。局部描述符表用来存储程序内部的段信息,而全局描述符表用来描述全局的段信息。x86 系列的处理器具有一种机制,可以在发生某些事件时自动切换局部描述符表,但是针对全局描述符表却没有这样的机制。

程序可以访问的内存通常被限制在一个段内。在 386 及以后的处理器中,由于 32 位的段内偏移与段大小的原因,有可能使段覆盖全部可寻址的空间,并且相关的段对用户来说也使透明的。

程序如果想使用某个段,需要在全局描述符表或者局部描述符表中找到该段对应的索引。这个索引被称为段选择子。为了使用相应的段,段选择子必须被首先加载到段寄存器。除了可以通过机器指令读取或者设置全局描述符表(还有中断描述符表)的内存地址外,指令所引用的内存地址存在于一个隐式的段,有时有两个。大部分情况下,默认的段寄存器可以通过在地址前面加一个段地址来替换。加载段选择子到段寄存器的过程中,程序会自动读取全局描述符表或者局部描述符表,并将相关信息保存到处理器中。在全局描述符表或者局部描述符表被加载后,对二者的修改并不会起作用,除非重新将相应的表加载到寄存器。

64 位下全局描述符表

全局描述符表在 64 位下仍然是合法可用的。相应的寄存器位数从 48 位扩展到了 80 位,64 位的段选择子是平坦的、无限制的(从 0x0000000000000000 到 0xFFFFFFFFFFFFFFFF)。64 位版本的 Windows 仍然禁止对全局表述附表的 hook 操作,如果进行这种操作会引发一个系统错误。

=========================================================================

386 处理器保护机制的重要方面就是全局描述符表。全局描述符表定义了一些内存区域的基本访问权限。可以使用全局描述符表中一个表项来描述一个段非法访问异常,这样,在进程进行了非法操作时,内核可以有机会终止进程。现代的大部分操作系统使用分页来实现这个机制:这种机制更加通用,给了上层更加大的灵活性。全局描述符表同样可以定义一块内存区域是可执行的,还是普通数据。全局描述符表有能力定义任务状态段(TSS)。TSS 用来实现基于硬件的多任务,这里不讨论。但是需要说明的是 TSS 并不是实现多任务的唯一方法。

GRUB 已经为我们加载了一个全局描述符表,如果我们重写了 GRUB 已经加载了的内存(全局描述符表所占空间),我们会破坏全局描述符表,并会引发一个 “triple fault” 错误。后果是引起机器重启。我们应该在有权限访问的内存中定义自己的全局描述符表,从而避免这个问题。这就需要我们重建全局描述符表,告诉处理器全局描述符表的位置,最后需要重新设置CS, DS, ES, FS, and GS,将其对应到我们自己的全局描述符表。CS 通常被称为代码段寄存器。代码段寄存器可以向处理器提供代码段在全局描述符表中偏移量,同时还提供了当前可执行代码的访问权限。同样,DS 向处理器提供了当前数据的访问权限。ES, FS, GS 只是改变 DS,对我们来说不重要。

全局描述符表是一个每个表项 64 位的表。每个表项定义了可以使用的内存区域的:起始,长度,访问权限。通用规则是:全局描述符表的第一个表项是 0,是一个空的描述符。段寄存器不应该被设置为 0,否则引发一个保护错,保护错是处理器的一种保护机制。保护错与处理器的其它保护机制在 http://www.osdever.net/bkerndev/Docs/isrs.htm 有详细介绍。

每个表项还定义了处理器当前正在运行的代码是在系统层(Ring0)还是在应用程序层(Ring3)。还有其它的 Ring,但是不重要。现在主要的操作系统都只使用 Ring0 与 Ring3。作为一个基本规则:如果应用程序访问 Ring0 ,会引发一个异常。这是为了让应用程序不会使系统崩溃。在全局描述符表部分所涉及的 Ring,主要是定了处理器是否可以执行某些特权指令。某些指令是特权级的,意味着只能在高特权的 Ring 中执行。例如:cli,sti 会禁用或者启用中断。如果允许应用程序使用 cli 与 sti,那它就可以终止内核的运行。

全局描述服表项

7 6 5 4 3 0
P DPL DT Type
P - Segment is present? (1 = Yes)
DPL - Which Ring (0 to 3)
DT - Descriptor Type
Type - Which type?
 
7 6 5 4 3 0
G D 0 A Seg Len. 19:16
G - Granularity (0 = 1byte, 1 = 4kbyte)
D - Operand Size (0 = 16bit, 1 = 32-bit)
0 - Always 0
A - Available for System (Always set to 0) 

在作为练习的系统内核中,我们会定义一个具有 3 个表项的全局描述符表。为什么 3 个?我们需要一个 'dummy' descriptor,来演示处理的保护特性。我们还需要一个代码段,一个数据段。我们使用 lgdt 指令来让处理器重新加载全局描述符表。使用 lgdt 指令需要一个指针,该指针指向一个 48 位的结构。48 位结构的前 16 位定义了全局描述符表的大小,剩下的 32 位是全局描述符表在内存中的起始地址。

我们可以简单的使用具有 3 个元素的数组来定义全局描述符表。

全局描述符表的一个实现:


#include < system.h > /* 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();


; 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!


/* 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(); }


=========================================================================

在 Intel 架构的处理器中,更确切的说是在保护模式下,内存管理与中断服务程序的控制是通过描述符表(tables of descriptors)来实现的。每个描述符的表项保存了处理器在某个时间要用的信息(例如:服务例程,任务,代码,数据等等)。如果你试着为段寄存器设置一个新值,处理器会进行关于安全与访问控制的检查。一旦通过检查,处理器会在内部寄存器中缓存这些值。

Intel 系列的处理器定义了三张表:中断描述符表,全局表述附表,局部描述符表。可以通过 LIDT,LGDT,LLDT 三个指令来加载这三张表。在大多数情况下,系统只是在启动时告诉处理器这三张表的位置,然后在以后的运行过程中通过指针来读取或者修改这三张表。

全局描述符表中应该存放什么信息?

如果完成的话,表中应该保存如下信息:

× 处理器从不引用的空指针。如果不设置,某些模拟器会抱怨缺少 limit exception。某些情况下,只是在这个位置保存指向全局描述符表自身的指针。

× 代码段描述子。对于内核来说,这个表项的类型为 0x9A。

× 数据段描述子。因为无法向代码段中写数据,因此需要添加数据段,类型为 0x92。

× 任务状态描述子。最好为这个段保留一定空间。

× 其它描述子空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值