x86 保护模式与特权

最后更新: 2022-12-24 15:42:13 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

x86 保护模式结合了基于描述符的分段、受控控制转移、分页和特权指令。长模式减少了普通分段的作用,但仍保留描述符表、特权级别、中断门和任务状态段(TSS)。操作系统使用这些机制进入内核并隔离用户执行,而页表则在当前 x86-64 系统中提供主要的内存保护边界。

表寄存器 #

全局描述符表(GDT)包含由逻辑处理器共享的代码、数据和系统描述符。GDTR存储GDT的线性基址和字节限制。按照惯例,GDT条目0是空描述符,因此索引为0的选择子不能用于普通代码或数据访问。

局部描述符表(LDT)是通过LDTR选择的可选表。LDTR包含GDT中LDT系统描述符的选择子,并缓存解析后的基址、限制和属性。在传统架构中,任务可以拥有自己的LDT,但当前的64位操作系统很少使用LDT。

IDTR存储中断描述符表(IDT)的线性基址和字节限制。IDT按异常或中断向量索引,而不是按普通段选择子索引。其条目是门,用于标识处理程序代码并控制进入语义。

任务寄存器(TR)包含GDT中TSS描述符的选择子,并缓存该描述符。LGDT、LIDT、LLDT和LTR会在架构规定的特权和类型检查下加载表状态。

选择子与隐藏缓存 #

16位段选择子包含索引、表指示器(TI)和请求特权级别(RPL):

text
15                              3 2   1 0
+--------------------------------+---+---+
|             index              | TI|RPL|
+--------------------------------+---+---+

TI=0选择GDT,TI=1选择当前LDT。处理器将索引乘以描述符大小,检查表限制,获取描述符,并验证其类型、存在位以及该操作所需的特权。

CS、SS、DS、ES、FS和GS不仅公开选择子,还包含基址、限制、类型和访问权限的隐藏缓存。内存引用使用该缓存,而不会再次读取描述符表。因此,重写GDT或LDT条目不会改变已加载的段;软件必须重新加载选择子,或执行架构要求的转换。

LDTR和TR也会缓存解析后的描述符。改变GDT、LDT或TSS的上下文切换必须考虑这些缓存,而不能把可见选择子视为完整状态。

段描述符 #

传统保护模式的代码和数据描述符长度为8字节。它们编码32位基址、可选使用4 KiB粒度的20位限制、描述符特权级别(DPL)、存在位、描述符类别以及类型相关标志。

  • S=1表示代码或数据描述符;S=0表示系统描述符或门。
  • 代码描述符选择可执行、可读和一致代码行为。
  • 数据描述符选择可写和向下扩展行为。
  • G缩放限制,而D/B根据描述符类型控制默认操作数大小或栈大小。
  • 在长模式中,L表示64位代码段;与D组合的无效配置会被拒绝。

分段会将描述符基址加到指令的有效偏移上,并在分页前检查限制和访问权限。平坦的32位模型使用基址为零、限制较大的段,同时保留这些检查。

在64位模式中,普通地址生成会将CS、SS、DS和ES的基址视为零,并且不应用它们的传统限制。FS和GS保留分别控制的64位基址。代码段类型、CS特权以及选定的SS规则仍具有架构意义。

系统描述符与门 #

系统描述符用于标识LDT、可用或忙碌的TSS以及传统门。IA-32使用8字节的TSS和LDT描述符。在64位模式中,TSS和LDT描述符占用16字节,以便容纳64位基址;高8字节会占用下一个GDT槽。

调用门指定目标代码选择子和偏移量,为向同级或更高特权级别执行远调用提供经过检查的路径。传统调用门可以将配置数量的参数复制到新栈上。远返回会在另一组检查下反转特权转换。

IDT条目可以是中断门、陷阱门,或者在传统模式下是任务门。中断门和陷阱门包含处理程序选择子和偏移量。中断门在进入时清除IF;陷阱门保留它。64位中断门或陷阱门还包含一个IST选择器,用于紧急栈或专用栈。

特权值与有效检查 #

x86定义了编号为0到3的四个特权级别。编号越小,特权越高。当前特权级别(CPL)由CS的低两位表示;内核通常运行在CPL 0,应用程序运行在CPL 3。环1和环2存在,但操作系统设计不要求使用它们。

每个描述符都有一个DPL,每个选择子都携带一个RPL。对于许多数据访问,处理器会形成有效特权级别:

text
EPL = max(CPL, RPL)

访问普通数据段要求EPL <= DPL,并且还必须通过类型和存在性检查。由于数值越大表示特权越低,RPL可以有意降低选择子的权限。SS加载遵循更严格的规则:选择子RPL和可写数据段DPL必须与CPL匹配。

代码转移根据具体机制使用不同规则:

  • 直接远转移到非一致代码段不会任意降低CPL;其DPL必须符合所要求的当前特权关系。
  • 一致代码段允许数值相同或特权较低的CPL执行,只要其DPL允许,但CPL保持不变。这种转移不会授予对目标DPL数据的访问权限。
  • 调用门选择子会使用CPL和RPL相对于门DPL进行检查,然后由该门控制进入目标代码段。
  • 软件INT n会根据CPL检查IDT门DPL。处理器异常和外部中断不会把门DPL用作调用者授权检查,因为软件并未通过INT n调用它们。

没有单一的CPL/DPL比较可以描述每种操作。段加载、一致代码、门、任务切换、IRET、端口I/O和特权指令各自定义了不同的检查规则。

受控转换 #

用户代码不能加载环0代码选择子,然后跳转到任意内核地址。门、异常、中断、任务机制或配置好的系统调用指令会建立受控入口点和保存的返回状态。

对于进入数值更低CPL的中断门或异常门,处理器从当前TSS获取目标特权级别的栈指针。随后切换栈,保存旧栈上下文和返回状态,并从门的处理程序开始执行。具体的栈帧、可选错误代码和保存值取决于执行模式和向量。

同一特权级别的门通常保留当前栈。在64位模式中,即使CPL没有变化,非零的IDT IST字段也会选择其配置的IST栈。此后,内核入口代码必须保存其余寄存器、建立ABI不变量、验证用户控制的状态,并安全处理嵌套进入。

使用IRET返回时,会验证并恢复保存的目标。返回到特权较低的CPL时,还会恢复外层栈。软件不得将未经检查的选择子、标志或指令指针放入中断返回帧,因为仅依靠返回时检查无法表达内核策略。

中断与系统调用 #

异常是指令执行的同步后果。外部中断通过中断控制器到达,并在优先级、屏蔽状态和处理器状态允许时被传递。两者最终都会选择IDT向量,但只有部分异常会压入架构定义的错误代码。

SYSCALL/SYSRET和SYSENTER/SYSEXIT避免了一般的IDT门查找,并使用特定于模型的寄存器建立进入和返回状态。64位模式下的SYSCALL将下一条RIP保存到RCX,将RFLAGS保存到R11,加载配置的代码选择子和入口RIP,并屏蔽选定的标志。它不会切换RSP;内核入口桩必须在不暴露不安全中间状态的情况下切换到可信栈。SYSENTER具有不同的配置栈语义。寄存器参数和错误约定属于操作系统的系统调用ABI,而不是x86本身。

调用门仍然是架构的一部分,但当前内核通常使用系统调用指令和中断门,因为这些路径适合平坦地址空间和软件定义的入口帧。分页、SMEP、SMAP、保护键和其他控制机制会补充描述符检查。

IA-32硬件任务 #

80286和IA-32保护模式架构将硬件任务定义为由TSS和TSS描述符表示的状态。IA-32 TSS存储EIP、EFLAGS、通用寄存器、段选择子、CR3、LDT选择子、特权级别0到2的栈指针,以及I/O权限位图偏移量。

硬件任务切换可以通过以下方式启动:

  • 远调用CALL或跳转JMP到TSS描述符。
  • 通过任务门进行远转移。
  • IDT条目为任务门的中断或异常。
  • 当EFLAGS.NT表示嵌套任务时执行IRET。

处理器会验证目标,将选定的出站状态保存到当前TSS,加载目标TSS,按要求标记描述符的忙状态,加载TR及其他任务状态,并设置CR0中的任务切换标志。具体的忙位、链接和异常行为取决于启动机制。

由CALL、任务门或中断任务门启动的嵌套任务切换,可以将旧TSS选择子写入目标TSS的回链,并设置嵌套任务(NT)标志。NT=1的IRET会沿回链恢复之前的任务。回链是架构控制状态,而不是由调度器维护的进程列表;格式错误或不一致的任务状态可能产生任务段异常。

当前操作系统使用软件切换被调度线程,因为硬件任务切换具有固定的状态布局和副作用,而内核还必须管理调度器元数据、地址空间策略、每CPU状态、浮点和向量状态、调试状态以及实现特定的缓解措施。软件切换可以只保存内核上下文和ABI所需的状态。IA-32内核仍然使用TSS实现特权栈和I/O位图功能,但不会使用硬件任务切换进行调度。

长模式下的TSS使用 #

64位模式不支持硬件任务切换,但每个逻辑处理器仍需要有效的64位TSS才能正常运行内核。TSS包含:

  • RSP0、RSP1和RSP2:当中断或异常在未选择IST的情况下进入相应的更高特权CPL时使用。
  • 七个中断栈表指针,即IST1到IST7。
  • I/O权限位图偏移量。

IDT门的三位IST字段选择七个IST指针中的一个。当当前栈可能不可用,或处理程序需要独立栈时,IST很有用,例如双重错误处理路径。IST硬件会加载新的栈指针,但不会提供完整的软件嵌套策略。入口代码仍必须防止有限紧急栈被无控制地重复使用或溢出。

I/O权限位图可以根据IOPL和模式规则,允许或拒绝低特权代码按端口执行IN和OUT。其位置和边界由TSS描述符限制导出,因此描述符、偏移量、位图和终止字节必须保持一致地构造。

TSS描述符仍然是GDT中的16字节系统描述符。LTR将其加载到TR,设置或观察规定的忙类型,并缓存其基址和限制。每个逻辑处理器通常拥有自己的TSS,因为特权进入需要处理器本地的可信栈。加载TR是进入处理的初始化,而不是调度器上下文切换。

长模式注意事项 #

长模式保留标准的CPL 0/CPL 3内核-用户划分、代码描述符、IDT门、GDT、TR和TSS。它从普通64位地址处理中移除了大多数段基址和限制的影响,并省略了硬件任务切换。因此,页表的用户/超级用户、可写和禁止执行权限构成主要的地址空间边界。

描述符状态仍不能省略。64位代码段必须具有有效描述符,中断门包含代码选择子,特权进入使用TSS栈状态,而兼容模式会应用额外的传统描述符语义。启动代码和内核代码应遵循处理器厂商当前的系统编程手册,因为保留字段和模式转换顺序属于架构要求。

主要参考资料 #

评论

在 GitHub 上阅读或评论(发表评论需要登录)