GDT Tutorial

来自osdev
Zhang3讨论 | 贡献2022年1月20日 (四) 03:27的版本 (创建页面,内容为“{{Rating|1}} 在 Intel Architecture 中,更确切地说,在 protected mode 中,大多数 memory managementInterrupt Service Routines 都是通过描述符表来控制的。 每个描述符存储关于CPU在某个时间可能需要的单个对象 (例如,服务例程、任务、代码或数据块,无论什么) 的信息。 例如,如果您尝试将新值加载到 段寄存器 中,则CPU需要执行…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索
难度等级
Difficulty 1.png
初学者

Intel Architecture 中,更确切地说,在 protected mode 中,大多数 memory managementInterrupt Service Routines 都是通过描述符表来控制的。 每个描述符存储关于CPU在某个时间可能需要的单个对象 (例如,服务例程、任务、代码或数据块,无论什么) 的信息。 例如,如果您尝试将新值加载到 段寄存器 中,则CPU需要执行安全和访问控制检查,以查看您是否实际上有权访问该特定内存区域。 一旦执行了检查,有用的值 (例如最低和最高地址) 就会缓存在CPU的不可见寄存器中。

英特尔定义了3种类型的表: Interrupt Descriptor Table (取代 IVT) 、全局描述符表 (GDT) 和本地描述符表。 每个表分别通过 LIDTLGDTLLDT 指令定义为 (大小,线性地址) 到CPU。 在大多数情况下,操作系统只是在启动时告诉这些表的位置,然后简单地通过指针写入/读取表。

词汇表

Segment
具有一致属性的逻辑上连续的内存块 (CPU的说法)
Segment Register
您的CPU的寄存器,它引用了用于特殊用途的段 (例如 SSCSDS ...)
Selector
对可以加载到段寄存器中的描述符的引用; 选择器是描述符表项的偏移量。 这些条目有8个字节长。 因此,位3及以上仅声明描述符表条目偏移,而位2指定此选择器是GDT还是LDT选择器 (LDT位设置,GDT位清除),和位0-1声明需要对应描述符表项的DPL字段的Ring级。 如果没有,则发生一般保护故障; 如果确实对应,则相应地更改所用选择器的CPL级别。
Descriptor
告诉CPU给定段的属性的内存结构 (表的一部分)

= 在GDT里放什么 =

基础

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

  • 处理器从未引用的空描述符。 某些仿真器 (例如Bochs) 会抱怨如果您没有出现限制异常。 有些使用此描述符来存储指向GDT本身的指针 (与LGDT指令一起使用)。 空描述符是8字节宽,指针是6字节宽,所以它可能只是这个完美的地方。
  • 一个代码段描述符 (对于您的内核,它应该具有type = 0x9A)
  • 数据段描述符 (您不能写入代码段,因此请使用type = 0x92添加它)
  • TSS 段描述符 (相信我,至少保留一个位置)
  • 如果需要,可以容纳更多细分段 (例如用户级别,LDT,更多ts,等等)

Sysenter/Sysexit

如果您使用的是Intel SYSENTER/SYSEXIT 例程,则GDT的结构必须如下:

  • 前面的任何描述符 (空描述符、特殊内核的东西等)
  • 一个 DPL 0代码段描述符 (SYSENTER 将使用的那个)
  • 一个DPL 0数据段描述符 (用于 SYSENTER 堆栈)
  • DPL 3代码段 (用于 SYSEXIT 之后的代码)
  • DPL 3数据段 (用于 SYSEXIT 之后的用户模式堆栈)
  • 任何其他描述符

DPL 0代码段的段被加载到 MSR 中。 其他的是根据该值计算的。 有关更多信息,请参阅 SYSENTERSYSEXIT 的英特尔说明参考。

您存储在那里的实际值将取决于您的系统设计。

Flat设置

您想要完整的4个GiB地址未翻译: 只需使用

GDT[0] = {.base=0, .limit=0, .type=0};                     // Selector 0x00 cannot be used
GDT[1] = {.base=0, .limit=0xffffffff, .type=0x9A};         // Selector 0x08 will be our code
GDT[2] = {.base=0, .limit=0xffffffff, .type=0x92};         // Selector 0x10 will be our data
GDT[3] = {.base=&myTss, .limit=sizeof(myTss), .type=0x89}; // You can use LTR(0x18)

请注意,在此模型中,由于代码和数据段重叠,因此实际上并未保护代码免受覆盖。

= 小内核设置 =

如果您希望 (出于特定原因) 将代码和数据清楚地分开 (假设两者都有4 MiB,也从4 MiB开始),只需使用:

GDT[0] = {.base=0, .limit=0, .type=0};                      // Selector 0x00 cannot be used
GDT[1] = {.base=0x400000, .limit=0x3fffff, .type=0x9A}; // Selector 0x08 will be our code
GDT[2] = {.base=0x800000, .limit=0x3fffff, .type=0x92}; // Selector 0x10 will be our data
GDT[3] = {.base=&myTss, .limit=sizeof(myTss), .type=0x89};  // You can use LTR(0x18)

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

= 怎么做 =

禁用中断

如果启用了它们,请将其关闭,否则您将遇到麻烦。

填表

GDT[] 的上述结构并不完整。 对于向后兼容286的GDT,描述符的实际结构有点混乱。 基址在3个不同的字段上分割,您不能编码任何您想要的限制。 另外,在这里和那里,如果你想让事情正常工作,你需要正确设置这些标志。

/**
 * \param target A pointer to the 8-byte GDT entry
 * \param source An arbitrary structure describing the GDT entry
 */
void encodeGdtEntry(uint8_t *target, struct GDT source)
{
    // Check the limit to make sure that it can be encoded
    if ((source.limit > 65536) && ((source.limit & 0xFFF) != 0xFFF)) {
        kerror("You can't do that!");
    }
    if (source.limit > 65536) {
        // Adjust granularity if required
        source.limit = source.limit >> 12;
        target[6] = 0xC0;
    } else {
        target[6] = 0x40;
    }
    
    // Encode the limit
    target[0] = source.limit & 0xFF;
    target[1] = (source.limit >> 8) & 0xFF;
    target[6] |= (source.limit >> 16) & 0xF;
    
    // 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;
    
    // And... Type
    target[5] = source.type;
}

当然,您可以对其进行硬编码,而不是在运行时将其转换。 此代码假定您只需要32位段。

告诉CPU表的位置

这里需要一些汇编示例。 虽然您可以使用 内联程序集,但 LGDTLIDT 所期望的内存打包使编写小型程序集例程变得更加容易。 如上所述,您将使用 LGDT 指令加载基地址和GDT的限制。 由于基址应该是一个线性地址,因此您需要根据当前的 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

从平面,保护模式

“Flat” 表示数据段的基数为0 (无论分页是打开还是关闭)。 例如,如果您只是被 GRUB 引导,情况就是这样。 您应该将其称为 setGdt(GDT,sizeof(GDT))

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

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

来自非flat保护模式

如果您的数据段具有非零基数 (例如,您在分段技巧中使用的是 较高的半内核), 您必须在上面序列的 “MOV EAX,...” 和 “MOV...,EAX” 指令之间 “ 添加EAX,base_of_your_data_segment_which_you_should_know”。

重新加载段寄存器

无论您对GDT做什么,都不会对CPU产生影响,直到您将选择器加载到段寄存器中。 您可以使用以下方法执行此操作:

reloadSegments:
   ; Reload CS register containing code selector:
   JMP   0x08:.reload_CS ; 0x08 points at the new code selector
.reload_CS:
   ; Reload data segment registers:
   MOV   AX, 0x10 ; 0x10 points at the new data selector
   MOV   DS, AX
   MOV   ES, AX
   MOV   FS, AX
   MOV   GS, AX
   MOV   SS, AX
   RET

可以找到上述代码的解释 here

LDT

与GDT (全局描述符表) 非常相似,LDT (“本地” 描述符表) 包含用于内存段描述,调用门等的描述符。 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进行操作系统开发,除非您设计了许多不同的段来存储。

The IDT and why it's needed

如上所述,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;
}

另见

文章

Threads

External Links