简介
本章介绍 IA-32 体系结构的任务管理功能。这些功能只有在处理器以保护模式运行时才可用。 此篇文章翻译自Intel® 64 和 IA-32 架构软件开发者手册,第3卷,第7章节中的任务管理部分。手工翻译,如有疏漏错误,务必以原卷为主。
如有Windows内核基础,即汇编基础,段机制基础,内存管理基础,阅读体验更佳。
第 7 章 任务管理
本章介绍 IA-32 体系结构的任务管理功能。这些功能只有在处理器以保护模式运行时才可用。
本章主要介绍 32 位任务和 32 位 TSS 结构。有关 16 位任务和 16 位 TSS 结构的信息,请参见第 7.6 节 "16 位任务状态段 (TSS)"。有关 64 位模式下任务管理的特定信息,请参见第 7.7 节 "64 位模式下的任务管理"。
7.1 任务管理概览
任务是处理器可以调度、执行和挂起的工作单元。它可用于执行程序、任务或进程、操作系统服务工具、中断或异常处理程序,或内核或执行实用程序。
IA-32 体系结构提供了一种机制,用于保存任务状态、调度任务执行以及从一个任务切换到另一个任务。在保护模式下运行时,所有处理器的执行都在任务内进行。即使是简单的系统也必须定义至少一个任务。更复杂的系统可以使用处理器的任务管理功能来支持多任务应用程序。
7.1.1 任务结构
任务由两部分组成:任务执行空间和任务状态段(TSS)。任务执行空间由代码段、堆栈段和一个或多个数据段组成(见图 7-1)。如果操作系统或执行程序使用处理器的特权级保护机制,任务执行空间也会为每个权限级别提供单独的堆栈。
TSS 规定了组成任务执行空间的段,并提供了任务状态信息的存储空间。在多任务系统中,TSS 还提供了连接任务的机制。
任务由其 TSS 的段选择子标识。当任务被加载到处理器中执行时,TSS 的段选择子、基地址、限制和段描述符属性会被加载到任务寄存器中(参见第 2.4.4 节 "任务寄存器 (TR)")。
如果任务执行了分页,则任务使用的页目录基地址将被加载到控制寄存器 CR3中。
7.1.2 任务状态
以下项目定义了当前执行任务的状态:
• 任务的当前执行空间,由段寄存器(CS、DS、SS、ES、FS 和 GS)中的段选择子定义。
• 通用寄存器的状态。
• EFLAGS 寄存器的状态。
• EIP 寄存器的状态。
• 控制寄存器 CR3 的状态。
• 任务寄存器的状态。
• LDTR 寄存器的状态。
• I/O 映射基地址和 I/O 映射(包含在 TSS 中)。
• 指向权限 0、1 和 2 堆栈(包含在 TSS 中)的堆栈指针。
• 链接到先前执行的任务(包含在 TSS 中)。
• 影子堆栈指针 (SSP) 的状态。
在调度任务前,除了任务寄存器的状态外,所有这些项目都包含在任务的 TSS 中。此外,TSS 中不包含 LDTR 寄存器的全部内容,只包含 LDT 的段选择子。
7.1.3 执行任务
软件或处理器可以通过以下方式之一调度任务的执行:
• 使用 CALL 指令明确调用任务。
• 使用 JMP 指令明确跳转到任务。
• 处理器对中断处理程序任务的隐式调用。
• 隐式调用异常处理任务。
• 当 EFLAGS 寄存器中的 NT 标志位被设置时,通过 IRET 指令发起的返回。
所有这些调度任务的方法都通过指向任务门(task gate)或任务状态段(TSS)的段选择子来确定要调度的任务。使用 CALL 或 JMP 指令调度任务时,指令中的选择子可以直接选择 TSS,也可以选择包含 TSS 选择子的任务门。当调度一个任务来处理中断或异常时,中断或异常的 IDT 条目必须包含一个任务门(task gate),该任务门保存指向中断或异常处理任务的 TSS 的选择子。
当任务被分派执行时,当前运行的任务和分派的任务之间会发生任务切换。在任务切换期间,当前正在执行的任务的执行环境(称为任务状态或上下文)会保存在其 TSS 中,任务的执行也会暂停。然后,被调度任务的上下文被加载到处理器中,该任务的执行从新加载的 EIP 寄存器所指向的指令开始。如果任务自系统初始化以来没有被执行过,EIP 将指向任务代码的第一条指令;否则,EIP 会指向该任务最后一次执行时的下一条指令。
如果当前执行的任务(调用任务)调用了被分派的任务(被调用任务),则调用任务的 TSS 段选择子将存储在被调用任务的 TSS 中,以提供返回调用任务的链接。
对于所有 IA-32 处理器,任务都不是递归的。任务不能调用或跳转到自身。
可以通过任务切换到处理程序任务来处理中断和异常。在这里,处理器执行任务切换以处理中断或异常,并在从中断处理程序任务或异常处理程序任务返回时自动切换回被中断的任务。这种机制还能处理在中断任务期间发生的中断。
作为任务切换的一部分,处理器还可以切换到另一个 LDT(局部描述符表),使每个任务都能为基于 LDT 的段提供不同的逻辑到物理地址映射。在任务切换时,页面目录基础寄存器(CR3)也会被重新加载,从而使每个任务都能拥有自己的页表。这些保护设施有助于隔离任务,防止它们相互干扰。
如果不使用保护机制,处理器就无法在任务之间提供保护。即使是使用多级权限保护的操作系统也是如此。一个运行在权限级别 3 的任务,如果与其他权限级别 3 的任务使用相同的 LDT 和页表,就可能访问代码并破坏其他任务的数据和堆栈。
使用任务管理功能处理多任务应用程序是可选的。多任务可在软件中处理,每个软件定义的任务在单个 IA-32 架构任务的上下文中执行。
如果启用了影子堆栈(shadow stack),则任务的 SSP (堆栈指针)位于 32 位 TSS 中偏移量 104 处的 4 个字节,当与该 TSS 相关联的任务发生任务切换时,处理器将使用该 SSP 建立任务的 SSP。请注意,处理器不会将发起任务切换的任务的 SSP 写入该任务的 TSS,而是将前一个任务的 SSP 推入新任务的影子堆栈中。
7.2 任务管理数据结构
处理器定义了五个数据结构,用于处理与任务相关的活动:
• 任务状态段(TSS)。
• 任务门描述符。
• TSS 描述符。
• 任务登记。
• EFLAGS 寄存器中的 NT 标志。
在保护模式下运行时,必须为至少一个任务创建 TSS 和 TSS 描述符,并将 TSS 的段选择子加载到任务寄存器中(使用 LTR 指令)。
7.2.1 任务-状态段 (TSS)
还原任务所需的处理器状态信息保存在称为任务状态段(TSS)的系统段中。图 7-2 显示了为 32 位 CPU 设计的任务状态段(TSS)格式。TSS 的字段主要分为两类:动态字段和静态字段。
有关 16 位 Intel 286 处理器任务结构的信息,请参见第 7.6 节 "16 位任务状态段 (TSS)"。有关 64 位模式任务结构的信息,请参见第 7.7 节 "64 位模式下的任务管理"。
在任务切换期间暂停任务时,处理器会更新动态字段。以下是动态字段:
• 通用寄存器字段 - 任务切换前 EAX、ECX、EDX、EBX、ESP、EBP、ESI 和 EDI 寄存器的状态。
• 段选择子字段 - 任务切换前存储在 ES、CS、SS、DS、FS 和 GS 寄存器中的段选择子。
• EFLAGS 寄存器字段 - 任务切换前 EFLAGS 寄存器的状态。
• EIP(指令指针)字段 - 任务切换前 EIP 寄存器的状态。
• 上一任务链接字段 - 包含上一任务 TSS 的段选择子(由调用、中断或异常启动的任务切换时更新)。该字段(有时也称为后置链接字段)允许使用 IRET 指令将任务切换回前一任务。
处理器会读取静态字段,但通常不会更改它们。这些字段是在创建任务时设置的。以下是静态字段:
• LDT 段选择子字段 - 包含任务 LDT 的段选择子。
• CR3 控制寄存器字段 - 包含任务要使用的页面目录的基本物理地址。控制寄存器 CR3 也称为页面目录基寄存器(PDBR)。
• 权限级别-0、-1 和 -2 的堆栈指针字段 - 这些堆栈指针由堆栈段的段选择子(SS0、SS1 和 SS2)和堆栈偏移量(ESP0、ESP1 和 ESP2)组成的逻辑地址构成。需要注意的是,这些字段中的值对于特定任务来说是静态的;而如果任务内发生堆栈切换,SS 和 ESP 值将会发生变化。
• T(调试陷阱)标志(字节 100,位 0)- 设置 T 标志后,当发生任务切换到该任务时,处理器将引发调试异常(参见第 17.3.1.5 节 "任务切换异常条件")。
• I/O 映射基址字段 - 包含从 TSS 基址到 I/O 允许位映射和中断重定向位映射的 16 位偏移。存在时,这些映射以较高地址存储在 TSS 中。I/O 映射基地址指向 I/O 允许位映射的起点和中断重定向位映射的终点。有关 I/O 权限位映射的更多信息,请参见《*英特尔® 64 和 IA-32 体系结构软件**开发人员手册》第 1 卷*第 19 章 "输入/输出"。有关中断重定向位图的详细说明,请参见第 20.3 节 "虚拟-8086 模式下的中断和异常处理"。
• 影子堆栈指针 (SSP) - 包含任务的影子堆栈指针。任务的影子堆栈应在任务 SSP 指向的地址(偏移量 104)处有一个上司影子堆栈标记。使用 CALL/JMP 指令切换到该影子堆栈时,将验证该标记并使其繁忙;使用 IRET 指令切换出该任务时,将释放该标记。
如果使用分页:
• 与上一个任务的 TSS、当前任务的 TSS 以及每个任务的描述符表项相对应的页面都应标记为可读/可写。
• 如果在启动任务切换之前,内存中已存在包含这些结构的页面,则任务切换的速度会更快。
7.2.2 TSS 描述符
TSS 与所有其他段一样,由段描述符定义。图 7-3 显示了 TSS 描述符的格式。TSS 描述符只能放在 GDT 中,不能放在 LDT 或 IDT 中。
如果试图使用设置了 TI 标志(表示当前 LDT)的段选择子访问 TSS,则会在 CALL 和 JMP 期间产生一般保护异常 (#GP);在 IRET 期间产生无效 TSS 异常 (#TS)。如果试图将 TSS 的段选择器加载到段寄存器中,也会产生一般保护异常。
类型字段中的忙标志 (B) 表示任务是否繁忙。一个忙碌的任务当前正在运行或被挂起。值为 1001B 的类型字段表示任务处于非活动状态;值为 1011B 的类型字段表示任务处于繁忙状态。任务是不可递归的。处理器使用忙碌标志来检测尝试调用一个正在执行被中断的任务的情况。为了确保每个任务只有一个忙碌标志,每个任务状态段(TSS)应该只通过一个 TSS 描述符来指向。
基址、限制、DPL 字段,以及粒度和存在标志在任务状态段(TSS)描述符中的功能与它们在数据段描述符中的作用相似(参见第3.4.5节“段描述符”)。
- 当 G 标志 为 0 时,在32位TSS的TSS描述符中,限制字段的值必须大于或等于 67H,这是TSS的最小大小减去1字节。如果尝试切换到一个TSS描述符的限制小于67H的任务,则会触发无效TSS异常(#TS)。
- 如果TSS中包含I/O权限位图,或者操作系统存储了额外的数据,则需要更大的限制值。
- 处理器在任务切换时并不检查限制是否大于67H;然而,在访问I/O权限位图或中断重定向位图时,处理器会进行检查。
任何可以访问 TSS 描述符的程序或过程(即其 CPL 在数值上等于或小于 TSS 描述符的 DPL)都可以通过调用或跳转来分派任务。
在大多数系统中,TSS 描述符的 DPL 设置为小于 3 的值,因此只有权限软件才能执行任务切换。不过,在多任务应用程序中,某些 TSS 描述符的 DPL 可设置为 3,以允许在应用程序(或用户)权限级别进行任务切换。
7.2.3 64 位模式下的 TSS 描述符
在 64 位模式下,不支持任务切换,但 TSS 描述符仍然存在。64 位 TSS 的格式参见第 7.7 节。
在 64 位模式下,TSS 描述符扩展为 16 个字节(见图 7-4)。这种扩展也适用于 64 位模式下的 LDT 描述符。表 3-2 提供了段类型字段的编码信息。
7.2.4 任务寄存器
任务寄存器(Task Register)保存当前任务的TSS(任务状态段)的16位段选择子及整个段描述符信息(包括32位基址(在IA-32e模式下为64位)、16位段限制和描述符属性)(参见图2-6)。这些信息是从当前任务的GDT(全局描述符表)中的TSS描述符复制过来的。图7-5展示了处理器如何通过任务寄存器访问TSS的信息。
任务寄存器有两个部分:可见部分(可以被软件读取和修改)和不可见部分(由处理器维护,软件无法访问)。可见部分的段选择子指向GDT中的TSS描述符。处理器使用任务寄存器的不可见部分来缓存TSS的段描述符信息。将这些值缓存到寄存器中可以提高任务的执行效率。
LTR(加载任务寄存器)和STR(存储任务寄存器)指令用于加载和读取任务寄存器的可见部分:
LTR 指令将一个段选择子(源操作数)加载到任务寄存器中,该选择子指向GDT中的TSS描述符。随后,它会将任务寄存器的不可见部分加载为来自TSS描述符的信息。LTR是一个特权指令,只有当CPL为0时才能执行。它通常在系统初始化期间用来给任务寄存器设置初始值。之后,当发生任务切换时,任务寄存器的内容会被隐式更改。
STR 指令将任务寄存器的可见部分存储到通用寄存器或内存中。此指令可以由任何特权级的代码执行,以识别当前正在运行的任务。然而,它通常仅由操作系统软件使用。(如果CR4.UMIP = 1,则只有当CPL为0时,才能执行STR。)
在处理器上电或复位时,段选择子和基地址会被设置为默认值0,限制被设置为FFFFH。
7.2.5 任务门描述符
任务门描述符(Task-gate descriptor)提供了对任务的间接、受保护的引用(参见图7-6)。它可以放置在GDT(全局描述符表)、LDT(局部描述符表)或IDT(中断描述符表)中。任务门描述符中的TSS段选择符字段指向GDT中的TSS描述符。此选择符中的RPL(请求特权级)字段不被使用。
任务门描述符的DPL(描述符特权级)控制在任务切换过程中对TSS描述符的访问。当一个程序或过程通过任务门调用或跳转到一个任务时,指向任务门的任务门选择符的CPL(当前特权级)和RPL字段必须小于或等于任务门描述符的DPL。需要注意的是,当使用任务门时,目标TSS描述符的DPL不再被使用。
一个任务可以通过任务门描述符(task-gate descriptor)或TSS描述符(TSS descriptor)来访问。这两种结构都满足以下需求:
需要任务只有一个忙碌标志 — 由于任务的忙碌标志存储在TSS描述符中,因此每个任务应该只有一个TSS描述符。然而,可能会有多个任务门引用同一个TSS描述符。
需要提供选择性访问任务的机制 — 任务门满足这一需求,因为它们可以存在于LDT中,并且可以具有与TSS描述符不同的DPL(描述符特权级)。如果一个程序或过程没有足够的特权级来访问GDT中的任务的TSS描述符(通常其DPL为0),它可能通过一个具有更高DPL的任务门访问该任务。任务门使操作系统在限制对特定任务的访问时具有更大的灵活性。
需要由独立任务处理中断或异常 — 任务门也可以存在于IDT(中断描述符表)中,从而允许中断和异常通过处理程序任务来处理。当一个中断或异常向量指向一个任务门时,处理器会切换到指定的任务。
图7-7说明了LDT中的任务门、GDT中的任务门和IDT中的任务门如何都可以指向同一个任务。
7.3 任务切换
在四种情况之一中,处理器会将执行任务转移到另一个任务:
• 当前程序、任务或过程执行 JMP 或 CALL 指令到 GDT 中的 TSS 描述符。
• 当前程序、任务或过程执行 JMP 或 CALL 指令到 GDT 或当前 LDT 中的任务门描述符。
• 中断或异常向量指向 IDT 中的任务门描述符。
• 当 EFLAGS 寄存器中的 NT 标志被设置时,当前任务将执行 IRET。
JMP、CALL、IRET指令以及中断和异常,都是用来重定向程序执行的机制。通过引用TSS描述符或任务门(当调用或跳转到任务时),或检查NT标志的状态(执行IRET指令时),决定是否发生任务切换。
当处理器进行任务切换时,它执行以下操作:
- 获取新任务的TSS段选择子,该选择子可以作为JMP或CALL指令的操作数,从任务门获取,或从前一个任务的链接字段获取(对于由IRET指令启动的任务切换)。
- 检查当前(旧)任务是否允许切换到新任务。数据访问特权规则适用于JMP和CALL指令。当前(旧)任务的CPL和新任务段选择子的RPL必须小于或等于所引用的TSS描述符或任务门的DPL。异常、中断(以下句子中提到的中断除外)以及IRET和INT1指令被允许无视目标任务门或TSS描述符的DPL来切换任务。对于由INT n、INT3和INTO指令生成的中断,如果DPL小于CPL,将会检查并引发一般保护异常(#GP)。1
- 检查新任务的TSS描述符是否被标记为存在并且具有有效的限制值(大于或等于67H)。如果任务切换是由IRET启动的,并且当前CPL启用了影子栈(shadow stack),那么SSP必须与8字节对齐,否则将会生成#TS(当前任务TSS)故障。如果CR4.CET为1,则新任务的TSS必须是32位TSS,并且新任务的TSS限制值必须大于或等于107字节,否则会生成#TS(新任务TSS)故障。
- 检查新任务是否可用(通过调用、跳转、中断或异常)或是否处于忙碌状态(通过IRET返回)。
- 检查当前(旧)TSS、新TSS以及所有在任务切换中使用的段描述符是否已经加载到系统内存中。
- 保存当前(旧)任务的状态到当前任务的TSS。处理器通过任务寄存器找到当前TSS的基地址,然后将以下寄存器的状态保存到当前TSS中:所有通用寄存器、段寄存器中的段选择符、暂时保存的EFLAGS寄存器的影像,以及指令指针寄存器(EIP)。
- 加载任务寄存器,将新任务TSS的段选择符和描述符加载到任务寄存器中。
- 如果启用了CET(控制扩展表),处理器将执行以下影子栈操作:
- 读取新任务的TSS信息:
- 处理器首先从新任务的TSS中读取CS(代码段)和EFLAGS。
- 处理EFLAGS中的虚拟模式(VM)标志:
- 如果EFLAGS.VM被设置为1,则新任务的CPL被设置为3(即用户模式)。
- 否则,新任务的CPL设置为CS.RPL。
- 根据任务切换的来源进行条件判断:
- 如果任务切换是由CALL指令、异常或中断引发的,并且当前CPL启用了影子栈,则需要检查任务的权限变化。
- 如果新任务的CPL小于当前任务的CPL,并且当前任务CPL为3,则IA32_PL3_SSP被设置为SSP(用户 → 管理员)。
- 否则,pushCsLipSsp被设置为1,表示没有权限变化(管理员 → 管理员;管理员 → 用户)。
- 同时,保存新任务的栈指针和指令指针。
- 如果任务切换是由CALL指令、异常或中断引发的,并且当前CPL启用了影子栈,则需要检查任务的权限变化。
- 任务切换由IRET指令触发的情况:
- 如果任务切换是由IRET指令触发,并且当前CPL启用了影子栈:
- 如果新任务的CPL与当前任务CPL相同,或两者都小于3,或新任务CPL小于3而当前任务CPL为3,则没有权限变化,或者从管理员模式切换到管理员模式,或者从用户模式切换到管理员模式。
- 在这些情况下,从影子栈中加载相应的CS、EIP和SSP值。
- 如果任务切换是由IRET指令触发,并且当前CPL启用了影子栈:
- 清除当前影子栈的忙碌标志:
- 如果SSP对齐到8字节,表示栈指针已经正确对齐,则清除当前任务在影子栈中的忙碌标志。
- 通过使用shadow_stack_lock_cmpxchg8b指令,保证SSP指向的忙碌位被正确更新。
- 如果SSP对齐到8字节,表示栈指针已经正确对齐,则清除当前任务在影子栈中的忙碌标志。
- 最终设置SSP:
- 完成上述步骤后,SSP被更新并返回到正常状态。
- 读取新任务的TSS信息:
- 加载TSS状态到处理器:
- 任务切换时,处理器将从新任务的TSS中加载状态,包括LDTR寄存器、PDBR(控制寄存器CR3)、EFLAGS寄存器、EIP寄存器、通用寄存器和段选择子。如果在加载过程中发生错误,可能会破坏架构状态。(如果分页未启用,则PDBR值会从新任务的TSS中读取,但不会加载到CR3中。)
- 清除任务的忙碌标志(B):
- 如果任务切换是由JMP或IRET指令引发的,处理器会清除当前任务(旧任务)TSS描述符中的忙碌标志(B);如果是由CALL指令、异常或中断引发的,则忙碌标志(B)保持不变。
- 处理NT标志:
- 如果任务切换是由IRET指令引发的,处理器会清除暂时保存的EFLAGS寄存器中的NT标志;如果是由CALL或JMP指令、异常或中断引发的,则NT标志在保存的EFLAGS图像中保持不变。
- 设置NT标志:
- 如果任务切换是由CALL指令、异常或中断引发的,处理器会在从新任务加载的EFLAGS中设置NT标志;如果是由IRET指令或JMP指令引发的,则NT标志将反映从新任务加载的EFLAGS中的NT状态。
- 设置新任务的忙碌标志(B):
- 如果任务切换是由CALL指令、JMP指令、异常或中断引发的,处理器会在新任务的TSS描述符中设置忙碌标志(B);如果是由IRET指令引发的,则忙碌标志保持不变。
- 加载和合格化段选择子关联的描述符:
- 处理器会加载与段选择子关联的描述符,并对其进行合格化。如果加载或合格化过程中发生错误,将发生在新任务的上下文中,可能会破坏架构状态。
- 如果启用了CET,处理器执行以下影子栈操作:
**步骤 1:**
如果当前CPL启用了影子栈或者当前CPL启用了间接分支跟踪,
则:
**步骤 1.1:** 如果 EFLAGS.VM = 1,
则执行 `#TSS(新任务TSS)`;
结束;
**步骤 2:**
如果当前CPL启用了影子栈,
且任务切换由 CALL 指令、JMP 指令、中断或异常发起 (* 切换栈 *):
**步骤 2.1:** `new_SSP ← 从 TSS 偏移量 104 加载 4 字节`
**步骤 2.2:** 验证新的 SSP 合法性:
如果 `new_SSP & 0x07 != 0`,
则执行 `#TSS(新任务TSS)`;
结束;
**步骤 2.3:** 设置期望的 token 值:`期望的 token 值 = SSP`
(* 繁忙 - 位0 - 必须清除 *)
设置新的 token 值:`新的 token 值 = SSP | BUSY_BIT`
(* 设置繁忙位 - 位0 *)
**步骤 2.4:** 如果 `shadow_stack_lock_cmpxchg8b(SSP, 新的 token 值, 期望的 token 值) != 期望的 token 值`,
则执行 `#TSS(新任务TSS)`;
结束;
**步骤 2.5:** 更新 SSP:`SSP = new_SSP`
**步骤 2.6:** 如果 `pushCsLipSsp = 1` (* 从用户 → 用户 或 从特权 → 特权 或 从特权 → 用户 的调用、int、异常 *):
使用 8 字节推送将 `tempSsCS`、`tempSsLip`、`tempSsSSP` 压入影子栈;
结束;
**步骤 3:**
如果任务切换由 IRET 发起,
且 `verifyCsLIP = 1`:
**步骤 3.1:** 执行 64 位比较;
CS 填充为 64 位;
CSBASE + EIP 填充为 64 位;
**步骤 3.2:** 如果 `tempSsCS` 和 `tempSsLIP` 与 `CS` 和 `CSBASE + EIP` 不匹配,
则执行 `#CP(远程返回/IRET)`;
结束;
**步骤 4:**
如果启用了影子栈(`ShadowStackEnabled(CPL)`):
**步骤 4.1:** 如果 `verifyCsLIP == 0`,
设置 `tempSSP = IA32_PL3_SSP`;
**步骤 4.2:** 如果 `tempSSP & 0x03 != 0`,
则执行 `#CP(远程返回/IRET)`
// 验证是否按 4 字节对齐;
**步骤 4.3:** 如果 `tempSSP[63:32] != 0`,
则执行 `#CP(远程返回/IRET)`;
**步骤 4.4:** 更新 SSP:`SSP = tempSSP`;
**步骤 5:**
如果启用了结束分支(`EndbranchEnabled(CPL)`):
**步骤 5.1:** 如果任务切换由 CALL 指令、JMP 指令、中断或异常发起,
且 `CPL = 3`,
则:
设置 `IA32_U_CET.TRACKER = 等待结束分支`
设置 `IA32_U_CET.SUPPRESS = 0`
否则,
设置 `IA32_S_CET.TRACKER = 等待结束分支`
设置 `IA32_S_CET.SUPPRESS = 0`
**结束。**
16. 开始执行新任务。
(对于异常处理程序而言,新任务的第一条指令似乎并未被执行。)
如果所有检查和保存操作成功完成,处理器将提交任务切换。如果在步骤1到步骤8中发生不可恢复的错误,处理器不会完成任务切换,并确保处理器返回到发起任务切换指令执行前的状态。
如果在步骤9中发生不可恢复的错误,架构状态可能会被破坏,但处理器会尝试在之前的执行环境中处理该错误。如果在提交点之后(在步骤13中)发生不可恢复的错误,处理器将完成任务切换(不会执行额外的访问和段可用性检查),并在开始执行新任务之前生成适当的异常。
如果在提交点之后发生异常,异常处理程序必须在允许处理器开始执行新任务之前完成任务切换。有关异常发生后对任务切换影响的更多信息,请参见第6章,“中断 10 - 无效TSS异常 (#TS)”。
当前正在执行的任务的状态在成功进行任务切换时始终会被保存。如果任务被恢复,执行将从保存的 EIP 值指向的指令开始,并且寄存器将恢复为任务暂停时的值。
在任务切换时,新任务的特权级不会继承自暂停任务的特权级。新任务从 CS 寄存器的 CPL 字段指定的特权级开始执行,该值从 TSS 中加载。由于任务通过各自的地址空间和 TSS 隔离开来,并且特权规则控制访问 TSS,因此软件不需要在任务切换时执行显式的特权检查。
表 7-1 显示了处理器在切换任务时检查的异常条件。它还显示了如果检测到错误,为每个检查生成的异常以及错误代码所引用的段。 (表中的检查顺序是 P6 系列处理器使用的顺序,具体顺序是与模型相关的,对于其他 IA-32 处理器可能会有所不同。) 设计用来处理这些异常的异常处理程序如果尝试重新加载生成异常的段选择子,可能会遇到递归调用的情况。在重新加载选择子之前,应该修复异常的原因(或多个原因中的第一个)。
条件检查 | 异常2 | 错误代码参考3 |
---|---|---|
TSS 描述符的段选择子引用了 GDT,且在表的限制范围内。 | #GP | 新任务的TSS |
#TS(用于 IRET) | ||
P 位已在 TSS 描述符中设置。 | #NP | 新任务的TSS |
TSS 描述符不忙(未被占用)(由调用、中断或异常启动的任务切换)。 | #GP (用于 JMP、CALL、INT) | 任务的回链段描述符(TSS) |
TSS 描述符不忙(未被占用)(由 IRET 指令启动的任务切换)。 | #TS(用于 IRET) | 新任务的TSS |
TSS 段限制大于等于 108(32 位 TSS)或 44(16 位 TSS)。 | #TS | 新任务的TSS |
如果 CR4.CET = 1,则 TSS 段限制大于或等于 108(32 位 TSS)。4 | #TS | 新任务的 TSS |
如果启用了影子堆栈,且 SSP 未对齐到 8 字节(由 IRET 指令启动的任务切换)。5 | #TS | 当前任务的 TSS |
寄存器根据 TSS 中的值加载。 | ||
新任务的 LDT 段选择器有效 。6 | #TS | 新任务的 LDT |
如果代码段是非一致的,其 DPL 应等于其 RPL。 | #TS | 新代码段 |
如果代码段是一致的,则其 DPL 应小于或等于其 RPL。 | #TS | 新代码段 |
SS 段选择子有效。7 | #TS | 新堆栈段 |
堆栈段描述符中的 P 位已设置。 | #SS | 新堆栈段 |
堆栈段 DPL 应等于 CPL。 | #TS | 新堆栈段 |
在新任务的 LDT 描述符中设置 P 位。 | #TS | 新任务的 LDT |
CS 段选择子有效。8 | #TS | 新代码段 |
代码段描述符中的 P 位已设置。 | #NP | 新代码段 |
堆栈段 DPL 应等于其 RPL。 | #TS | 新堆栈段 |
DS、ES、FS 和 GS 段选择子是有效的。9 | #TS | 新数据段 |
可读取 DS、ES、FS 和 GS 段。 | #TS | 新数据段 |
在 DS、ES、FS 和 GS 段的描述符中设置 P 位。 | #NP | 新数据段 |
DS、ES、FS 和 GS 区段的 DPL 大于或等于 CPL(除非这些是一致性段)。 | #TS | 新数据段 |
任务中的影子栈指针未对齐到 8 字节(对于由调用、中断或异常发起的任务切换)。10 | #TS | 新任务的TSS |
如果 EFLAGS.VM=1 且启用了影子堆栈。11 | #TS | 新任务的TSS |
监督模式影子栈令牌验证失败(对于由调用、中断、跳转或异常发起的任务切换):12 | #TS | 新任务的TSS |
- 忙碌位已设置。 | ||
- 影子堆栈令牌中的地址与 TSS 中的 SSP 值不匹配。 | ||
如果任务切换由 IRET 启动,则旧任务影子堆栈中存储的 CS 和 LIP 与新任务的 CS 和 LIP 不匹配。13 | #CP | FAR-RET/IRET |
如果任务切换是由 IRET 发起的,并且新任务的 SSP 从旧任务的影子栈加载(如果新任务的 CPL < 3)或从 IA32_PL3_SSP 加载(如果新任务的 CPL = 3),则需通过以下检查:14 | #CP | FAR-RET/IRET |
- 未对齐至 4 字节。 | ||
- 超越了 4G。 |
控制寄存器 CR0 中的 TS(任务切换)标志在每次任务切换时被设置。系统软件使用 TS 标志来协调浮点单元在生成浮点异常时与处理器其余部分的动作。TS 标志表示浮点单元的上下文可能与当前任务的上下文不同。有关 TS 标志的功能和使用的详细描述,请参见第 2.5 节,“控制寄存器”。
7.4 任务链接
TSS(任务状态段)中的前一个任务链接字段(有时称为“回链”)和 EFLAGS 寄存器中的 NT 标志用于将执行返回到上一个任务。EFLAGS.NT = 1 表示当前执行的任务嵌套在另一个任务的执行中。
当 CALL 指令、一个中断或异常引发任务切换时:处理器将当前 TSS 的段选择子复制到新任务的 TSS 中的前一个任务链接字段,然后设置 EFLAGS.NT = 1。如果软件使用 IRET 指令来挂起新任务,处理器会检查 EFLAGS.NT = 1,然后使用前一个任务链接字段中的值返回到上一个任务。有关详细说明,请参见图 7-8。
当 JMP 指令引发任务切换时,新任务不是嵌套的。前一个任务链接字段不会使用,EFLAGS.NT = 0。在不希望发生嵌套时,使用 JMP 指令来调度新任务。
表 7-2 显示了在任务切换过程中,TSS 段描述符中的忙碌标志、NT 标志、前一个任务链接字段以及控制寄存器 CR0 中的 TS 标志。
NT 标志可以由在任何特权级别执行的软件修改。程序可以设置 NT 标志并执行 IRET 指令,这可能会随机调用当前任务 TSS 中前一个链接字段指定的任务。为了防止这种无效的任务切换成功,操作系统应当在它创建的每个 TSS 中将前一个任务链接字段初始化为 0。
标志或字段 | JMP 指令的作用 | CALL 指令或中断的影响 | IRET 指令的影响 |
---|---|---|---|
新任务忙 (B)标志。 | 标志已设置。之前一定是清除的。 | 标志已设置。之前一定是清除的。 | 没有变化。必须是已被设置。 |
旧任务的忙 (B)标志。 | 标志已清除。 | 无变化。标志目前已设置。 | 标志已清除。 |
新任务的 NT 标志。 | 设置为新任务 TSS 的值。 | 标志已设置。 | 设置为新任务 TSS 的值。 |
旧任务的 NT 标志。 | 无变化。 | 无变化。 | 标志已清除。 |
新任务的上一个任务链接字段。 | 无变化。 | 加载了旧任务TSS的选择子。 | 无变化。 |
旧任务的上一个任务链接字段。 | 无变化。 | 无变化。 | 无变化。 |
控制寄存器 CR0 中的 TS 标志。 | 标志已设置。 | 标志已设置。 | 标志已设置。 |
7.4.1 使用 "忙 "标志防止递归任务切换
TSS(任务状态段)只允许为一个任务保存一个上下文;因此,一旦任务被调用(调度),对该任务的递归(或重入)调用将导致任务的当前状态丢失。为了防止重入任务切换并随之丢失任务状态信息,TSS 段描述符中的忙碌标志(busy flag)被引入。处理器以如下方式管理忙碌标志:
- 调度任务时,处理器会设置新任务的忙碌标志。
- 在任务切换过程中,如果当前任务被放置在嵌套链中(即任务切换是由 CALL 指令、中断或异常引发的),当前任务的忙碌标志会保持设置。
- 当切换到新任务时(由 CALL 指令、中断或异常发起),如果新任务的忙碌标志已经设置,处理器会触发一个一般保护异常(#GP)。如果任务切换是由 IRET 指令发起的,则不会触发异常,因为处理器期望忙碌标志已经设置。
- 当任务通过跳转到新任务(由任务代码中的 JMP 指令发起)或由任务代码中的 IRET 指令终止时,处理器会清除忙碌标志,将任务返回到“未忙碌”状态。
处理器通过防止任务切换到自身或任何在嵌套任务链中的任务,从而防止递归任务切换。嵌套挂起任务链可以因多个调用、中断或异常而增长到任意长度。忙碌标志防止任务在该链中被调用。
在多处理器配置中,忙碌标志也可以使用,因为处理器在设置或清除忙碌标志时遵循 LOCK 协议(在总线或缓存中)。此锁机制防止两个处理器同时调用相同的任务。有关在多处理器应用中设置忙碌标志的更多信息,请参见第 8.1.2.1 节,“自动锁定”。
7.4.2 修改任务链路
在单处理器系统中,当需要从一系列链接的任务链中移除一个任务时,可以按照以下步骤进行:
- 禁用中断:为了防止任务切换或其他中断干扰任务链修改过程,首先禁用中断。
- 修改前一个任务链接字段:修改被抢占任务的 TSS 中的前一个任务链接字段。假设抢占任务是任务链中待移除任务的下一个任务(即更新后的任务)。将该链接字段更改为指向任务链中的下一个任务(即比待移除任务更老的任务)或者链中更老的任务。
- 清除忙碌(B)标志:清除待移除任务的 TSS 段描述符中的忙碌标志(B)。如果需要从链中移除多个任务,那么每个任务的忙碌标志都需要被清除。
- 启用中断:在修改任务链和清除标志后,重新启用中断,以恢复系统的正常中断处理。
在多处理器系统中,除了上述步骤外,还需要增加额外的同步和序列化操作,以确保在修改前一个任务链接字段和清除忙碌标志时,TSS 和其段描述符都已经被锁定。这是为了防止在多个处理器同时操作时发生竞争条件或数据不一致的问题。
额外的多处理器同步步骤:
- 锁定 TSS 和段描述符:确保在修改任务链时,TSS 和其段描述符在各个处理器中不会同时被访问。使用适当的锁机制(如总线锁定或缓存锁定)来确保对这些关键数据结构的独占访问。
- 防止竞态条件:在锁定期间,确保其他处理器无法修改任务链或段描述符,避免任务链被错误地更新。
7.5 任务地址空间
一个任务的地址空间由该任务可以访问的段组成。这些段包括 TSS 中引用的代码段、数据段、栈段和系统段,以及任务代码访问的任何其他段。这些段被映射到处理器的线性地址空间,线性地址空间又被映射到处理器的物理地址空间(无论是直接映射还是通过分页机制)。
TSS 中的 LDT 段字段 可用于为每个任务提供自己的局部描述符表(LDT)。为任务分配一个独立的 LDT,可以将该任务的地址空间与其他任务隔离开来,因为所有与该任务相关的段描述符都放置在该任务的 LDT 中。
此外,也可以让多个任务共享同一个 LDT。这是一种节省内存的方式,它允许特定的任务之间进行通信或相互控制,而无需放宽整个系统的保护边界。
由于所有任务都可以访问全局描述符表(GDT),因此也可以通过该表中的段描述符创建共享段。
如果启用了分页,TSS 中的 CR3 寄存器(PDBR)字段 允许每个任务拥有自己的一套页表,用于将线性地址映射到物理地址。或者,多个任务也可以共享同一套页表。
7.5.1 将任务映射到线性地址空间和物理地址空间
任务可以通过两种方式映射到线性地址空间和物理地址空间:
- 所有任务共享一个线性到物理地址空间的映射:
- 分页未启用时,这是唯一的选择。没有分页时,所有线性地址都映射到相同的物理地址。
- 分页启用时,这种线性到物理地址空间的映射通过使用一个分页目录(Page Directory)来为所有任务提供映射。当支持按需分页的虚拟内存时,线性地址空间可能超过可用的物理空间。
- 每个任务有自己独立的线性地址空间,该空间映射到物理地址空间:
- 这种映射方式通过为每个任务使用不同的分页目录来实现。由于在任务切换时会加载 PDBR(控制寄存器 CR3),因此每个任务可能拥有不同的分页目录。
- 不同任务的线性地址空间可以映射到完全不同的物理地址。如果不同的分页目录项指向不同的页表,而这些页表又指向不同的物理内存页面,那么任务之间就不会共享物理地址。
无论采用哪种方式映射任务的线性地址空间,所有任务的 TSS 必须位于物理空间的共享区域中,所有任务都能访问这个区域。这样做是为了确保在处理器读取和更新 TSS 时,TSS 地址的映射不会发生变化,确保任务切换时 TSS 的一致性。此外,GDT 映射的线性地址空间也应映射到物理空间的共享区域,否则 GDT 的作用将被削弱。图 7-9 展示了如何通过共享页表,使两个任务的线性地址空间在物理空间中重叠。
7.5.2 任务逻辑地址空间
为了实现任务之间的数据共享,可以使用以下技术来创建共享的逻辑到物理地址空间的映射:
通过 GDT 中的段描述符
- 所有任务都必须能够访问 GDT 中的段描述符。如果 GDT 中的某些段描述符指向线性地址空间中的段,而这些段映射到物理地址空间中的某个区域,这个区域是所有任务共享的,那么所有任务都可以共享这些段中的数据和代码。
- 这种方法的优点是全局共享,所有任务都可以访问这些共享的段,适合于需要多个任务访问相同数据或代码的情况。
通过共享的 LDT
- 如果两个或多个任务使用相同的 LDT,可以通过将它们的 TSS 中的 LDT 字段指向同一个 LDT 来实现共享。LDT 中的一些段描述符指向映射到物理地址空间中共享区域的段,那么这些任务就可以共享这些段中的数据和代码。
- 这种方法比通过 GDT 共享更具选择性,因为可以将共享限制在特定任务之间。系统中的其他任务可能会有不同的 LDT,这些 LDT 不允许它们访问共享的段。
通过映射到线性地址空间中公共地址的不同 LDT 中的段描述符
- 如果这些共享的线性地址空间区域被映射到每个任务的物理地址空间中的相同区域,这些段描述符允许任务之间共享这些段。此类段描述符通常称为“别名”(alias)。
- 这种共享方式比前两种方法更具选择性,因为不同 LDT 中的其他段描述符可能指向独立的线性地址,这些地址并不被共享。因此,只有指向共享线性地址区域的段描述符才能实现共享,其他段则不会共享。
7.6 16 位任务状态段(TSS)
32位IA-32处理器也支持类似于Intel 286处理器使用的16位TSS格式(见图7-10)。这种格式是为了与早期IA-32处理器上运行的软件保持兼容而设计的。关于16位TSS,以下信息非常重要:
- 不要使用16位TSS来实现虚拟8086任务。
- 16位TSS的有效段限制为2CH。
- 16位TSS不包含页面目录基地址字段,页面目录基地址是加载到控制寄存器CR3中的。16位任务不支持为每个任务设置单独的页表。如果调度了一个16位任务,前一个任务的页表结构将被使用。
- 16位TSS中不包含I/O基地址。I/O映射的功能不被支持。
- 当任务状态被保存到16位TSS时,EFLAGS寄存器的高16位和EIP寄存器的值将丢失。
- 当从16位TSS加载或保存通用寄存器时,寄存器的高16位会被修改并且不会保持不变。
7.7 64 位模式下的任务管理
在64位模式下,任务结构和任务状态与保护模式下的相似。然而,保护模式下的任务切换机制在64位模式中不被支持。任务管理和切换必须由软件执行。如果在64位模式下尝试以下操作,处理器将发出通用保护异常(#GP):
- 使用JMP、CALL、INT n、INT3、INTO、INT1或中断进行控制转移到TSS或任务门。
- 执行带有EFLAGS.NT(嵌套任务)位设置为1的IRET指令。
虽然64位模式下不支持硬件任务切换,但仍然必须存在一个64位任务状态段(TSS)。图7-11展示了64位TSS的格式。TSS保存了与64位模式相关的重要信息,这些信息与任务切换机制并不直接相关。以下是这些信息的内容:
- RSPn — 特权级0-2的堆栈指针(RSP)的完整64位规范格式。
- ISTn — 中断堆栈表(IST)指针的完整64位规范格式。
- I/O映射基地址 — 从64位TSS基址到I/O权限位图的16位偏移量。
操作系统在激活IA-32e模式后必须至少创建一个64位TSS。它必须执行LTR指令(在64位模式下)将TR寄存器加载为指向64位TSS的指针,该TSS负责64位模式程序和兼容模式程序的管理。
INT1的操作码是F1;INT n指令,其中n = 1,其操作码是CD 01。↩︎
#NP 表示段不存在异常,#GP 表示一般保护异常,#TS 表示无效-TSS 异常,#SS 表示堆栈故障异常。↩︎
错误代码包含一个该列中引用的段描述符的索引。↩︎
当 CET 启用时有效。↩︎
当 CET 启用时有效。↩︎
如果段选择子位于兼容的表类型(GDT 或 LDT)中,且地址在表的段限制范围内,并且指向兼容类型的描述符(例如,CS 寄存器中的段选择子仅在它指向代码段描述符时有效),则该段选择子有效。↩︎
错误代码包含一个该列中引用的段描述符的索引。↩︎
如果段选择子位于兼容的表类型(GDT 或 LDT)中,且地址在表的段限制范围内,并且指向兼容类型的描述符(例如,CS 寄存器中的段选择子仅在它指向代码段描述符时有效),则该段选择子有效。↩︎
如果段选择子位于兼容的表类型(GDT 或 LDT)中,且地址在表的段限制范围内,并且指向兼容类型的描述符(例如,CS 寄存器中的段选择子仅在它指向代码段描述符时有效),则该段选择子有效。↩︎
当 CET 启用时有效。↩︎
当 CET 启用时有效。↩︎
当 CET 启用时有效。↩︎
当 CET 启用时有效。↩︎
当 CET 启用时有效。↩︎