wiki.osdev.org 系列之(九)- GDT 教程

在 IA-32 和 x86-64 架构上,更准确地说是在保护模式或长模式下,中断服务例程和大量内存管理是通过描述符表来控制的。每个描述符都存储有关 CPU 在某个时间可能需要的单个对象的信息(例如,服务例程、任务、代码或数据块)。例如,如果您尝试将新值加载到段寄存器中,CPU 需要执行安全和访问控制检查,以查看您是否真的有权访问该特定内存区域。执行检查后,有用的值(例如最低和最高地址)将缓存在不可见的 CPU 寄存器中。

在这些体系结构中,共有三种此类表:全局描述符表、局部描述符表和中断描述符表(它取代了中断向量表)。每个表是通过 LGDT、LLDT 和 LIDT 指令分别使用它们的大小和到 CPU 的线性地址来定义的。在几乎所有用例中,这些表只在启动时放入内存一次,然后在需要时进行编辑。

关键词汇

Segment (段)

具有一致属性的逻辑上连续的内存块(从 CPU 的角度来看)。

段寄存器

CPU 的寄存器,它引用用于特定目的(CS、DS、SS、ES)或一般用途(FS、GS)的段

段选择器

对描述符的引用,您可以将其加载到段寄存器中;选择器是指向其条目之一的描述符表的偏移量。这些条目通常为 8 字节长,因此位 3 和更高位仅声明描述符表条目偏移量,而位 2 指定此选择器是 GDT 还是 LDT 选择器(LDT - 位设置,GDT - 位清除),位 0 - 1 声明需要对应描述符表条目的DPL字段的环级别。如果没有,则发生一般保护故障;如果它确实对应,则使用的选择器的 CPL 级别会相应更改。

段描述符

描述符表中的条目。这些是一种二进制数据结构,可以告诉 CPU 给定段的属性。

在 GDT 中放入什么

基本数据

出于明智的考虑,您应该始终将这些项目存储在您的GDT中:

  • 描述符表中的条目 0 或 Null Descriptor 永远不会被处理器引用,并且应该始终不包含任何数据。某些模拟器,比如 Bochs,当你的 GPT 中没有一个空条目时,就会抛出一个限制异常。有些人使用这个描述符来存储指向 GDT 本身的指针(与 LGDT 指令一起使用)。空描述符为 8 字节宽,指针为 6 字节宽,因此它可能是处理此问题的理想场所。
  • 一个 DPL 0 为0的代码段描述符(用于您的内核)
  • 数据段描述符(代码段不允许写入)
  • 一个任务状态段段描述符(它非常有用,至少有一个)
  • 如果需要,为更多的细分市场留出空间(例如,用户级、ldt、更多的TSS等等)

Flat / Long Mode Setup(平面/长模式设置)

如果您不希望使用分段将内存分成受保护的区域,您可以只使用一些段描述符。一个原因可能是您希望只使用分页来保护内存。同样,该模型在长模式下被 强制执行 ,因为忽略了基本值和限制值。

在这种情况下,唯一需要的段描述符是空描述符,以及用于特权级别、段类型和所需执行模式的每种组合的描述符,以及系统描述符。通常这将包括内核和用户模式的一个代码和一个数据段,以及一个任务状态段。

image.png

Small Kernel Setup(小内核设置)

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

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

image.png

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

这并不是一个值得推荐的设计,而是展示了如何考虑使用GDT来定义分离的线段。

SYSENTER / SYSEXIT(系统输入/系统退出)

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

如需更多信息,请参阅英特尔软件开发人员手册第2-B卷第4.3章:指令(M-U)中有关SYSENTER和SYSEXIT的章节。

image.png

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

如何设置 GDT

关中断

如果它们已启用,请务必将其关闭,否则您可能会遇到不良行为和异常。这可以通过 CLI 汇编指令来实现。

填表

GDT 的上述结构并未向您展示如何以正确的格式编写条目。为了向后兼容 286 的 GDT,描述符的实际结构有点混乱。基地址分为三个不同的字段,您无法编码任何您想要的限制。

void encodeGdtEntry(uint8_t *target, struct GDT source)
{
    // Check the limit to make sure that it can be encoded
    if (source.limit > 0xFFFFF) {kerror("GDT cannot encode limits larger than 0xFFFFF");}
 
    // Encode the limit
    target[0] = source.limit & 0xFF;
    target[1] = (source.limit >> 8) & 0xFF;
    target[6] = (source.limit >> 16) & 0x0F;
 
    // Encode the base
    target[2] = source.base & 0xFF;
    target[3] = (source.base >> 8) & 0xFF;
    target[4] = (source.base >> 16) & 0xFF;
    target[7] = (source.base >> 24) & 0xFF;
 
    // Encode the access byte
    target[5] = source.access_byte;
 
    // Encode the flags
    target[6] |= (source.flags << 4)
}

为了填写您的表格,您需要为每个条目使用一次此函数,其中 *target 指向段描述符的逻辑地址,而 source 是设计的包含必要信息的结构体。

当然,您可以在 GDT 中对值进行硬编码,而不是在运行时转换它们。

告诉 CPU 表在哪里

这里需要一些汇编。虽然您可以使用内联汇编,但 LGDT 和 LIDT 指令所期望的内存打包使得编写小型汇编例程变得更加容易。如上所述,您将使用 LGDT 指令加载 GDT 的基地址和限制。由于基地址应该是线性地址,因此您需要根据当前的 MMU 设置进行一些调整。

实模式

此处的线性地址应计算为 segment * 16 + offset. GDT 和 GDT_end 被假定为当前数据段中的符号。

gdtr DW 0 ; For limit storage 16 位
     DD 0 ; For base storage
 
setGdt:
   XOR   EAX, EAX	; 清空EAX
   MOV   AX, DS		; 将数据段地址赋值给 ax
   SHL   EAX, 4		; 左移四位
   ADD   EAX, ''GDT''	; ?加上 GDT 的偏移?
   MOV   [gdtr + 2], eax	; 将 EAX 中的值,赋值到 gdtr + 2 上,存储 base
   MOV   EAX, ''GDT_end''	; 将 GDT_end 赋值到 EAX
   SUB   EAX, ''GDT''	; 计算 GDT 的长度?
   MOV   [gdtr], AX	; 储存低16位,limit
   LGDT  [gdtr]		; 加载 GDT 地址到相应的不可见寄存器
   RET

保护模式,平面模型

“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]		; 取出 limit
   MOV   [gdtr], AX		; 存储 limit
   MOV   EAX, [ESP + 8]		; 取出 base
   MOV   [gdtr + 2], EAX	; 存储 base
   LGDT  [gdtr]			; 加载到 对应寄存器
   RET

长模式

在长模式下,Base 字段的长度是 8 个字节,而不是 4 个字节。同样,System V ABI 通过 RDI 和 RSI 寄存器传递前两个参数。因此,此示例代码可以调用为 setGdt(limit, base)。同样,在长模式下只能使用平面模型,因此无需考虑其他因素。

gdtr DW 0 ; For limit storage
     DQ 0 ; For base storage
 
setGdt:
   MOV   [gdtr], DI	; 存储 limit 32 位
   MOV   [gdtr+2], RSI	; 存储 base 64 位
   LGDT  [gdtr]
   RET

重新加载段寄存器

在将新的段选择器加载到段寄存器之前,您对 GDT 所做的任何事情都不会影响 CPU。对于这些寄存器中的大多数,该过程就像使用 MOV 指令一样简单,但是更改 CS 寄存器需要类似于跳转或调用其他地方的代码,因为这是更改其值的唯一方法。

保护模式

在这种情况下,重新加载 CS 就像在跳转指令之后直接执行到所需段的远跳转一样简单:

reloadSegments:
   ; Reload CS register containing code selector:
   JMP   0x08:.reload_CS ; 0x08 is a stand-in for your code segment
.reload_CS:
   ; Reload data segment registers:
   MOV   AX, 0x10 ; 0x10 is a stand-in for your data segment
   MOV   DS, AX
   MOV   ES, AX
   MOV   FS, AX
   MOV   GS, AX
   MOV   SS, AX
   RET

可以在此处找到对上述代码的解释。

长模式

在长模式中,改变 CS 的过程并不简单,因为不能使用远跳转。建议使用远返回:

reloadSegments:
   ; Reload CS register:
   PUSH 0x08                 ; Push code segment to stack, 0x08 is a stand-in for your code segment
   LEA RAX, [rel .reload_CS] ; Load address of .reload_CS into RAX
   PUSH RAX                  ; Push this value to the stack
   RETFQ                     ; Perform a far return, RETFQ or LRETQ depending on syntax
.reload_CS:
   ; Reload data segment registers
   MOV   AX, 0x10 ; 0x10 is a stand-in for your data segment
   MOV   DS, AX
   MOV   ES, AX
   MOV   FS, AX
   MOV   GS, AX
   MOV   SS, AX
   RET

LDT

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

由于它的内容在每个任务中可能是不同的,LDT 不适合放置诸如 TSS 或其他 LDT 描述符之类的系统内容:这些是 GDT 的独有属性。由于它意味着经常更改,因此用于加载 LDT 的命令与 GDT 和 IDT 加载命令有点不同。这些参数不是直接给出 LDT 的基地址和大小,而是存储在 GDT 的描述符中(具有适当的“LDT”类型),并给出该项的选择器。

image.png

请注意,对于 386+ 处理器,分页使 LDT 几乎过时,并且不再需要多个 LDT 描述符,因此您几乎可以放心地忽略 LDT 进行 OS 开发,除非您有意存储许多不同的段。

IDT 以及为什么需要它

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

IDT 将是您的内核序列中首先启用的功能之一,以便您可以捕获硬件异常、侦听外部事件等。有关 X86 系列中断的更多信息,请参阅中断

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

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

// Used for creating GDT segment descriptors in 64-bit integer form.
 
#include <stdio.h>
#include <stdint.h>
 
// Each define here is for a specific flag in the descriptor.
// Refer to the intel documentation for a description of what each one does.
#define SEG_DESCTYPE(x)  ((x) << 0x04) // Descriptor type (0 for system, 1 for code/data)
#define SEG_PRES(x)      ((x) << 0x07) // Present
#define SEG_SAVL(x)      ((x) << 0x0C) // Available for system use
#define SEG_LONG(x)      ((x) << 0x0D) // Long mode
#define SEG_SIZE(x)      ((x) << 0x0E) // Size (0 for 16-bit, 1 for 32)
#define SEG_GRAN(x)      ((x) << 0x0F) // Granularity (0 for 1B - 1MB, 1 for 4KB - 4GB)
#define SEG_PRIV(x)     (((x) &  0x03) << 0x05)   // Set privilege level (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;         // set limit bits 19:16
    descriptor |= (flag <<  8) & 0x00F0FF00;         // set type, p, dpl, s, g, d/b, l and avl fields
    descriptor |= (base >> 16) & 0x000000FF;         // set base bits 23:16
    descriptor |=  base        & 0xFF000000;         // set base bits 31:24
 
    // Shift by 32 to allow for low part of segment
    descriptor <<= 32;
 
    // Create the low 32 bit segment
    descriptor |= base  << 16;                       // set base bits 15:0
    descriptor |= limit  & 0x0000FFFF;               // set limit bits 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;
}

See Also

Articles


标题:wiki.osdev.org 系列之(九)- GDT 教程
作者:糖醋鱼
地址:https://expoli.tech/articles/2022/06/11/1655022123760.html

    评论
    0 评论
avatar

取消