type
status
slug
summary
tags
category
icon
password
new update day
Property
Oct 22, 2023 01:31 PM
created days
Last edited time
Oct 22, 2023 01:31 PM
在 IA-32 和 x86-64 架构上,更准确地说是在保护模式或长模式下,中断服务例程和大量内存管理是通过描述符表来控制的。每个描述符都存储有关 CPU 在某个时间可能需要的单个对象的信息(例如,服务例程、任务、代码或数据块)。例如,如果您尝试将新值加载到段寄存器中,CPU 需要执行安全和访问控制检查,以查看您是否真的有权访问该特定内存区域。执行检查后,有用的值(例如最低和最高地址)将缓存在不可见的 CPU 寄存器中。
在这些体系结构中,共有三种此类表:全局描述符表、局部描述符表和中断描述符表(它取代了中断向量表)。每个表是通过 LGDT、LLDT 和 LIDT 指令分别使用它们的大小和到 CPU 的线性地址来定义的。在几乎所有用例中,这些表只在启动时放入内存一次,然后在需要时进行编辑。

1 关键词汇

Segment (段)

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

段寄存器

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

段选择器

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

段描述符

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

2 在 GDT 中放入什么

2.1 基本数据

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

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

如果您不希望使用分段将内存分成受保护的区域,您可以只使用一些段描述符。一个原因可能是您希望只使用分页来保护内存。同样,该模型在长模式下被 强制执行 ,因为忽略了基本值和限制值。
在这种情况下,唯一需要的段描述符是空描述符,以及用于特权级别、段类型和所需执行模式的每种组合的描述符,以及系统描述符。通常这将包括内核和用户模式的一个代码和一个数据段,以及一个任务状态段。
表格样式
32bit
Offset
Use
Content
0x0000
Null Descriptor
Base = 0 Limit = 0x00000000 Access Byte = 0x00 Flags = 0x0
0x0008
Kernel Mode Code Segment
Base = 0 Limit = 0xFFFFF Access Byte = 0x9A Flags = 0xC
0x0010
Kernel Mode Data Segment
Base = 0 Limit = 0xFFFFF Access Byte = 0x92 Flags = 0xC
0x0018
User Mode Code Segment
Base = 0 Limit = 0xFFFFF Access Byte = 0xFA Flags = 0xC
0x0020
User Mode Data Segment
Base = 0 Limit = 0xFFFFF Access Byte = 0xF2 Flags = 0xC
0x0028
Task State Segment
Base = &TSS Limit = sizeof(TSS) Access Byte = 0x89 Flags = 0x0
64bit
Offset
Use
Content
0x0000
Null Descriptor
Base = 0 Limit = 0x00000000 Access Byte = 0x00 Flags = 0x0
0x0008
Kernel Mode Code Segment
Base = 0 Limit = 0xFFFFF Access Byte = 0x9A Flags = 0xA
0x0010
Kernel Mode Data Segment
Base = 0 Limit = 0xFFFFF Access Byte = 0x92 Flags = 0xC
0x0018
User Mode Code Segment
t Base = 0 Limit = 0xFFFFF Access Byte = 0xFA Flags = 0xA
0x0020
User Mode Data Segment
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
notion image

2.3 Small Kernel Setup(小内核设置)

如果您希望将内存分隔为受保护的代码和数据区域,则必须将表中每个条目的 Base 和 Limit 值设置为所需的格式。
例如,您可能希望有两个段,一个从 4MiB 开始的 4MiB 代码段和一个从 8MiB 开始的 4MiB 数据段,两者都只能由 Ring 0 访问。在这种情况下,您的 GDT 可能如下所示:
notion image
这意味着您在物理地址 4 MiB 加载的任何内容都将显示为 CS:0 处的代码,而您在物理地址 8 MiB 加载的内容将显示为 DS:0 处的数据。
这并不是一个值得推荐的设计,而是展示了如何考虑使用GDT来定义分离的线段。

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

如果您使用英特尔 SYSENTER/SYSEXIT 例程,GDT 必须包含四个特殊条目,第一个条目由IA32_SYSENTER_CS型号特定寄存器(MSR 0x0174)中的值指向。
如需更多信息,请参阅英特尔软件开发人员手册第2-B卷第4.3章:指令(M-U)中有关SYSENTER和SYSEXIT的章节。
notion image
这些段中存储的实际值将取决于您的系统设计。

3 如何设置 GDT

3.1 关中断

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

3.2 填表

GDT 的上述结构并未向您展示如何以正确的格式编写条目。为了向后兼容 286 的 GDT,描述符的实际结构有点混乱。基地址分为三个不同的字段,您无法编码任何您想要的限制。
为了填写您的表格,您需要为每个条目使用一次此函数,其中 *target 指向段描述符的逻辑地址,而 source 是设计的包含必要信息的结构体。
当然,您可以在 GDT 中对值进行硬编码,而不是在运行时转换它们。

3.3 告诉 CPU 表在哪里

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

3.3.1 实模式

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

3.3.2 保护模式,平面模型

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

3.3.3 保护模式,非平面模型

如果您的数据段具有非零基数,则您必须调整上述序列的指令以包括添加数据段的基数偏移量的能力,这对您来说应该是已知值。您可以将其作为参数传入,并将此函数称为 setGdt(limit, base, offset)

3.3.4 长模式

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

3.4 重新加载段寄存器

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

3.4.1 保护模式

在这种情况下,重新加载 CS 就像在跳转指令之后直接执行到所需段的远跳转一样简单:
可以在此处找到对上述代码的解释。

3.4.2 长模式

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

4 LDT

与 GDT(全局描述符表)非常相似,LDT(局部描述符表)包含用于内存段描述、调用门等的描述符。LDT 的好处是每个任务都可以有自己的 LDT,当您使用硬件任务切换时,处理器会自动切换到正确的LDT。
由于它的内容在每个任务中可能是不同的,LDT 不适合放置诸如 TSS 或其他 LDT 描述符之类的系统内容:这些是 GDT 的独有属性。由于它意味着经常更改,因此用于加载 LDT 的命令与 GDT 和 IDT 加载命令有点不同。这些参数不是直接给出 LDT 的基地址和大小,而是存储在 GDT 的描述符中(具有适当的“LDT”类型),并给出该项的选择器。
notion image
请注意,对于 386+ 处理器,分页使 LDT 几乎过时,并且不再需要多个 LDT 描述符,因此您几乎可以放心地忽略 LDT 进行 OS 开发,除非您有意存储许多不同的段。

5 IDT 以及为什么需要它

如上所述,IDT(中断描述符表)的加载方式与 GDT 大致相同,其结构大致相同,只是它只包含门而不包含段。每个门都提供对一段代码(代码段、特权级别和该段中代码的偏移量)的完整引用,该代码现在绑定到 0 到 255 之间的数字(IDT 中的插槽)。
IDT 将是您的内核序列中首先启用的功能之一,以便您可以捕获硬件异常、侦听外部事件等。有关 X86 系列中断的更多信息,请参阅中断

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

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

7 See Also

7.1 Articles

wiki.osdev.org 系列之(七)- 我应该按什么顺序做东西?wiki.osdev.org 系列之(九)- 全局描述符表 (GDT)