GDT Tutorial

来自osdev
跳到导航 跳到搜索
难度等级
Difficulty 1.png
初学者

IA-32x86-64 架构上,更准确地说,在 保护模式-Protected Mode长模式-Long Mode 中,控制中断服务例程 ISR-Interrupt Service Routines 和做好内存管理都需要通过描述符表(descriptors table)。 每个描述符存储CPU在某个时间点可能需要的单个目标(例如服务例程、任务、代码或数据块)信息。 例如,如果你尝试将新值加载到段寄存器中时, CPU需要执行安全和访问控制检查,以查看你是否实际上有权访问该特定内存区域。 一旦执行了检查,有用的值(如最低和最高地址)就会根据描述符,被读取并缓存到也许不可见的CPU寄存器中。

在这些体系结构上,有三种此类型的表:全局描述符表-Global Descripptor Table本地描述符表-Local DescrippTable中断描述符表(它取代了中断向量表-Interrupt Vector Table)。 上面每个表都用它们的大小和 线性地址进行了定义, 并分别通过 LGDTLLDTLIDT 指令发送到CPU。 在几乎所有的用例中,这些表都只在启动时放入内存一次,然后在需要时进行编辑。

必知词汇表

分段-Segment
逻辑上连续的内存块,从CPU的角度具有一致的属性。
段寄存器(Segment Register)
一类CPU的寄存器,它指向的是用于特定目的的段 (CSDSSSES) 或用于一般用途的 (FSGS)
段选择器-Segment Selector
对描述符(descriptor)的引用,你可以将其加载到段寄存器中; 选择器(selector)是指向描述符表(descriptor table)中条目(Entry)之一的偏移量。 这些条目通常为8字节长,因此仅第3位及以上用以声明描述符表条目偏移,而第2位指定该选择器是GDT还是LDT选择器(LDT位设置1,GDT位清除0), 同时第0-1位声明需要对应描述符表项的DPL字段的Ring级。 如果级别不对,则发生一般保护故障(General Protection Fault);如果它确实对应,则所用选择器的CPL安全级别会相应地改变。
段描述符-Segment Descriptor
描述符表中的条目。 这是一个二进制数据结构,告诉CPU给定段的属性。

在GDT中放入什么

基础内容

为了合理使用CPU,你应该始终将以下项目存储在GDT中:

  • 描述符表中的条目0,空描述符从不被处理器引用,并且应该始终不包含任何数据。 如果你没有Null Descriptor,某些模拟器 (例如Bochs) 会报限制异常(limit exceptions)。 有些人使用这个描述符来存储指向GDT本身的指针(与LGDT指令一起使用)。 空描述符为8字节宽,其中指针为6字节宽,因此它可能正是进行此操作的最佳位置。
  • 一个DPL 0 代码段 描述符 (用于你的内核)
  • 一个数据段描述符(不允许写入代码段)
  • 一个Task State Segment段描述符(至少有一个是非常有用的)
  • 如果需要,可以容纳更多细分段 (例如用户级别,LDT,更多TSS,等等)

Flat / Long Mode 设置

如果你不希望使用分段将内存分隔为多个受保护区域,则只需使用几个段描述符即可。 一个原因可能是你希望仅使用分页来保护内存。 此外,此设置方式在Long Mode中必须严格执行,因为base和limit值已经被忽略了。

在此方案中,唯一需要的段描述符-Segment Descriptor空描述符, 以及一个描述符(由期望的特权级别、段类型和执行模式的组合而成),以及系统描述符。 通常,这将包括内核和用户模式的一个代码段和一个数据段,以及一个Task State Segment

32-bit
选择器 用途 内容
0x0000 Null Descriptor Base = 0
Limit = 0x00000000
Access Byte = 0x00
Flags = 0x0
0x0008 内核模式代码段 Base = 0
Limit = 0xFFFFF
Access Byte = 0x9A
Flags = 0xC
0x0010 内核模式数据段 Base = 0
Limit = 0xFFFFF
Access Byte = 0x92
Flags = 0xC
0x0018 用户模式代码段 Base = 0
Limit = 0xFFFFF
Access Byte = 0xFA
Flags = 0xC
0x0020 用户模式数据段 Base = 0
Limit = 0xFFFFF
Access Byte = 0xF2
Flags = 0xC
0x0028 Task State Segment Base = &TSS
Limit = sizeof(TSS)
Access Byte = 0x89
Flags = 0x0
64-bit
选择器 用途 内容
0x0000 Null Descriptor Base = 0
Limit = 0x00000000
Access Byte = 0x00
Flags = 0x0
0x0008 内核模式代码段 Base = 0
Limit = 0xFFFFF
Access Byte = 0x9A
Flags = 0xA
0x0010 内核模式数据段 Base = 0
Limit = 0xFFFFF
Access Byte = 0x92
Flags = 0xC
0x0018 用户模式代码段 Base = 0
Limit = 0xFFFFF
Access Byte = 0xFA
Flags = 0xA
0x0020 用户模式数据段 Base = 0
Limit = 0xFFFFF
Access Byte = 0xF2
Flags = 0xC
0x0028 Task State Segment
(64-bit System Segment)
Base = &TSS
Limit = sizeof(TSS)
Access Byte = 0x89
Flags = 0x0

细分内核设置

如果你希望将内存分离到代码和数据的受保护区域中,则必须将表中每个条目的 BaseLimit 值设置为所需的格式。

例如,你可能希望有两个段,一个4MiB代码段从4MiB开始,另一个4MiB数据段从8MiB开始,这两个段都只能由Ring 0访问。 在这种情况下,你的GDT可能如下所示:

Small Kernel
选择器 用途 内容
0x0000 Null Descriptor Base = 0
Limit = 0x00000000
Access Byte = 0x00
Flags = 0x0
0x0008 内核模式代码段 Base = 0x00400000
Limit = 0x003FFFFF
Access Byte = 0x9A
Flags = 0xC
0x0010 内核模式数据段 Base = 0x00800000
Limit = 0x003FFFFF
Access Byte = 0x92
Flags = 0xC
0x0018 Task State Segment Base = &TSS
Limit = sizeof(TSS)
Access Byte = 0x89
Flags = 0x0

这意味着在物理地址4 MiB加载的内容将在CS:0处显示为代码,在物理地址8 MiB加载的内容将在DS:0处显示为数据。

以上设置并不是推荐的设计,但是展示了如何考虑使用GDT来定义独立的段。

SYSENTER / SYSEXIT

如果你使用的是英特尔 SYSENTER / SYSEXIT 例程(routines),GDT 必须包含四个特殊条目,第一个条目由 IA32_SYSENTER_CS 中的值指向 模型特定寄存器 (MSR 0x0174)。

有关详细信息,请参阅《英特尔软件开发人员手册》 第2-B卷 第4.3章:中的Chapter 4.3: Instructions (M-U)部分。

GDT
选择器 用途
前置的一些条目(Entry) Null描述符
内核段
等.
IA32_SYSENTER_CS + 0x0000 DPL 0 Code Segment
SYSENTER Code
IA32_SYSENTER_CS + 0x0008 DPL 0 Data Segment
SYSENTER Stack
IA32_SYSENTER_CS + 0x0010 DPL 3 Code Segment
SYSEXIT Code
IA32_SYSENTER_CS + 0x0018 DPL 3 Data Segment
SYSEXIT Stack
随后的一些条目 其他任何描述符

存储在这些段中的实际值将取决于你的系统设计。

如何设置GDT

禁用中断

如果启用了中断,需 “绝对确定” 将其关闭,否则你可能会遇到不希望的行为和异常。 这可以通过CLI汇编指令实现。

填写表

上面的 GDT 结构说明中还没有向你展示如何以正确的格式编写条目。 由于与286的GDT向后兼容,描述符的实际结构有点混乱。 Base地址分为三个不同的字段,并且你不能对随意选择的limit进行编码。

void encodeGdtEntry(uint8_t *target, struct GDT source)
{
    // 检查limit以确保可以对其进行编码
    if (source.limit > 0xFFFFF) {kerror("GDT cannot encode limits larger than 0xFFFFF");}

    // 对limit进行编码
    target[0] = source.limit & 0xFF;
    target[1] = (source.limit >> 8) & 0xFF;
    target[6] = (source.limit >> 16) & 0x0F;

    //对base进行编码
    target[2] = source.base & 0xFF;
    target[3] = (source.base >> 8) & 0xFF;
    target[4] = (source.base >> 16) & 0xFF;
    target[7] = (source.base >> 24) & 0xFF;

    // 编码access byte
    target[5] = source.access_byte;

    // 对各标志位进行编码
    target[6] |= (source.flags << 4)
}

为了填写GDT表,你需要为每个条目使用一次此函数,这里*target指向Segment Descriptor的逻辑地址,source是你设计的包含必要信息的结构体。

当然,你也可以在 GDTsource结构体 中对值实现进行硬编码,而不是在运行时转换它们。

告诉CPU表在哪里

设置CPU GDT表位置需要一些汇编。 虽然你可以使用 内联汇编,但 LGDTLIDT 指令所期望的是内存包,这使编写小型汇编例程来实现更加容易。 如上所述,你将使用LGDT指令加载Base和GDT的Limit。 由于基地址应该是线性地址,根据当前的MMU设置,你需要进行一些调整。

实模式

这里的线性地址应计算为 段*16+偏移量。 以下代码中 GDTGDT_end 被假定为当前数据段中的符号。

gdtr DW 0 ; For limit storage
     DD 0 ; For base storage

setGdt:
   XOR   EAX, EAX
   MOV   AX, DS
   SHL   EAX, 4
   ADD   EAX, ''GDT''
   MOV   [gdtr + 2], eax
   MOV   EAX, ''GDT_end''
   SUB   EAX, ''GDT''
   MOV   [gdtr], AX
   LGDT  [gdtr]
   RET

Protected Mode, Flat Model

“Flat”表示数据段的基地址为0(无论是否启用了分页)。 例如,如果你的代码刚刚被 GRUB 引导,就是这种情况。 在System V ABI中,参数在堆栈中按相反顺序传递,因此可以setGdt(limit, base)的函数调用可能类似于以下示例代码。

gdtr DW 0 ; For limit storage
     DD 0 ; For base storage

setGdt:
   MOV   AX, [esp + 4]
   MOV   [gdtr], AX
   MOV   EAX, [ESP + 8]
   MOV   [gdtr + 2], EAX
   LGDT  [gdtr]
   RET

Protected Mode, Non-Flat Model

如果你的数据段具有非零的base,则必须调整上述序列的指令,以包括添加数据段的base offset的功能,offset应该是你的已知值。 你可以将其作为参数传入,并将此函数调用为setGdt(limit, base, offset)

gdtr DW 0 ; For limit storage
     DD 0 ; For base storage

setGdt:
   MOV   AX, [esp + 4]
   MOV   [gdtr], AX
   MOV   EAX, [ESP + 8]
   ADD   EAX, [ESP + 12]
   MOV   [gdtr + 2], EAX
   LGDT  [gdtr]
   RET

Long Mode

Long Mode中,Base字段的长度是8个字节,而不是4个字节。 同样,System V ABI 通过 RDIRSI 寄存器传递前两个参数。 因此,这个示例代码可以这样setGdt(limit, base)调用。 此外,在长模式下,只有flat model是可能的,因此不必考虑其他情况。

gdtr DW 0 ; For limit storage
     DQ 0 ; For base storage

setGdt:
   MOV   [gdtr], DI
   MOV   [gdtr+2], RSI
   LGDT  [gdtr]
   RET

重新加载段寄存器

在将新的段选择器(Segment Selectors)加载到段寄存器(Segment Registers)中之前,对GDT所做的任何操作都不会对CPU产生影响。 对于这些寄存器中的大多数,过程与使用MOV指令一样简单,但是更改CS寄存器需要类似于jump或call别处的代码,因为这是更改其值的唯一方式。

保护模式

在这种情况下,直接在jump指令之后,重新加载CS与执行到所需段的far jump一样简单:

reloadSegments:
   ; 重新加载包含代码选择器的CS寄存器:
   JMP   0x08:.reload_CS ; 0x08代表是你的代码段
.reload_CS:
   ; 重新加载数据段寄存器:
   MOV   AX, 0x10 ; 0x10代码是你的数据段
   MOV   DS, AX
   MOV   ES, AX
   MOV   FS, AX
   MOV   GS, AX
   MOV   SS, AX
   RET

这里可以找到上述代码的详细解释。

Long Mode

Long Mode中,更改CS的过程并不简单,因为不能使用far jump。 建议改用far return来代替:

reloadSegments:
   ; Reload CS register:
   PUSH 0x08                 ; 将代码段推送到堆栈,0x08代表是你的代码段
   LEA RAX, [rel .reload_CS] ; 将.reload_CS的地址加载到RAX中
   PUSH RAX                  ; 将此值推入栈
   RETFQ                     ; 根据语法执行far return、RETFQ或LRETQ
.reload_CS:
   ; 重新加载数据段寄存器
   MOV   AX, 0x10 ; 0x10代表是你的数据段
   MOV   DS, AX
   MOV   ES, AX
   MOV   FS, AX
   MOV   GS, AX
   MOV   SS, AX
   RET

LDT(本地描述符表)

与GDT (全局描述符表) 非常相似,LDT (“本地” 描述符表) 包含了用于内存段描述,调用门(call gates)等的描述符。 LDT的好处是每个任务都可以有自己的LDT,并且当你使用硬件任务切换时,处理器会自动切换到正确的LDT。

由于其内容在每个任务中可能不同,因此LDT不是放置系统内容 (例如TSS或其他LDT描述符) 的合适位置: 这些只能放在是GDT中。 因为它需要经常更改,所以用于加载LDT的命令与GDT和IDT加载略有不同。 这些参数不是直接给出LDT的基地址和大小,而是存储在GDT的描述符中 (具有适当的 “LDT” 类型),并给出该条目的选择器。

               GDTR (base + limit)
              +-- GDT ------------+
              |                   |
SELECTOR ---> [LDT descriptor     ]----> LDTR (base + limit)
              |                   |     +-- LDT ------------+
              |                   |     |                   |
             ...                 ...   ...                 ...
              +-------------------+     +-------------------+

请注意,对于386+处理器,分页已经使LDT几乎过时,并且不再需要多个LDT描述符,因此在操作系统开发中几乎可以安全地忽略LDT,除非需要通过设计许多不同的段来进行存储。

IDT介绍以及为什么需要它

如上所述,IDT (中断描述符表) 的加载方式与GDT大致相同,其结构大致相同,只是它只包含调用门(gate)而不包含段。 每个门都给出一段代码的完整引用(代码段、特权级别和该段代码的偏移量),每段代码绑定到0到255之间的数字(IDT中的插槽-slot)。

IDT将是内核序列中最先启用的内容之一,这样你就可以进行硬件异常捕获、监听外部事件等。 有关X86系列中断的更多信息,请参见 中断

一些让你的生活更轻松的东西

用于轻松创建GDT条目的工具。

// 用于创建64位整数形式的GDT段描述符。
 
#include <stdio.h>
#include <stdint.h>
 
// 这里的每个定义都针对描述符中的特定标志。
// 请参阅英特尔文档,了解每项功能的说明。
#define SEG_DESCTYPE(x)  ((x) << 0x04) // 描述符类型 (系统为0,代码/数据为1)
#define SEG_PRES(x)      ((x) << 0x07) // Present
#define SEG_SAVL(x)      ((x) << 0x0C) // 可供系统使用
#define SEG_LONG(x)      ((x) << 0x0D) // Long mode
#define SEG_SIZE(x)      ((x) << 0x0E) // Size(16位为0,32位为1)
#define SEG_GRAN(x)      ((x) << 0x0F) // Granularity-粒度 (10b-1 mb为0,4KB-4gb为1)
#define SEG_PRIV(x)     (((x) &  0x03) << 0x05)   // 设置权限级别(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
 
void
create_descriptor(uint32_t base, uint32_t limit, uint16_t flag)
{
    uint64_t descriptor;
 
    // Create the high 32 bit segment
    descriptor  =  limit       & 0x000F0000;         // 设置limit位19:16
    descriptor |= (flag <<  8) & 0x00F0FF00;         // 设置类型,p,dpl,s,g,d/b,l和avl字段
    descriptor |= (base >> 16) & 0x000000FF;         // 设置base位23:16
    descriptor |=  base        & 0xFF000000;         //设置base位31:24
 
    // 移位32以启用段的低位部分
    descriptor <<= 32;
 
    // 创建低32位段
    descriptor |= base  << 16;                       // 设置base位15:0
    descriptor |= limit  & 0x0000FFFF;               // 设置limit位15:0
 
    printf("0x%.16llX\n", descriptor);
}
 
int
main(void)
{
    create_descriptor(0, 0, 0);
    create_descriptor(0, 0x000FFFFF, (GDT_CODE_PL0));
    create_descriptor(0, 0x000FFFFF, (GDT_DATA_PL0));
    create_descriptor(0, 0x000FFFFF, (GDT_CODE_PL3));
    create_descriptor(0, 0x000FFFFF, (GDT_DATA_PL3));
 
    return 0;
}

另见

文章

论坛主题

外部链接