x86汇编语言——任务隔离和切换
任务
之前的程序将初始化段,内核段和用户程序段描述符都存储在GDT中,而处理器建议每个任务都应当有自己的描述符表,称为局部描述符表LDT(local descriptor table)

LDT不止一个,处理器使用局部描述符表寄存器LDTR追踪和访问LDT,LDTR只有一个并指向当前任务的LDT,当任务发生切换时,LDTR的内容被更新
和GDTR一样,LDTR包含了32位线性基地址字段和16位段界限字段,以指示当前LDT的位置和大小。段选择子是16位的,而且只有高13位被用做索引号来访问GDT或者LDT,所以,每个LDT所能容纳的描述符个数为2^13,即8192个。或者换句话说,每个LDT只能定义8192个段。又因为每个描述符的长度是8字节,LDT的长度最大为64KB
段选择子的位2是指示器,若TI=0,表示从GDT中加载描述符,TI=1,表示从LDT中加载描述符

在多任务的环境中,当任务发生切换时,必须保护就任务的运行状态包括通用寄存器,段寄存器,栈指针,指令指针寄存器状态寄存器等等,为了保存任务的状态,并在下次重新执行时恢复,用额外的内存区域保存相关信息,即任务状态段TSS(table state segment)
处理器用TR(任务寄存器)指示当前任务的TSS

全局空间和局部空间
每个任务实际上包括两个部分:全局部分和私有部分。
- 全局部分是所有任务共有的,含有操作系统的软件和库程序,以及可以调用的系统服务和数据;
- 私有部分则是每个任务各自的数据和代码,与任务所要解决的具体问题有关,彼此并不相同
任务实际上是在内存中运行的,所以,所谓的全局部分和私有部分,其实是地址空间的划分,即全局地址空间和局部地址空间,简称全局空间和局部空间。
虚拟内存管理:同一块物理内存,可以让多个任务,或者每个任务的不同段来使用。当执行或者访问一个新的段时,如果它不在物理内存中,而且也没有空闲的物理内存空间来加载它,那么,操作系统将挑出一个暂时用不到的段,把它换出到磁盘中,并把那个腾出来的空间分配给马上要访问的段并修改段的描述符,使之指向这段内存空间。下一次,当被换出的那个段马上又要用到时,再按相同的办法换回到物理内存。
特权保护
引入LDT和TSS,只是从任务层面上进一步强化了分段机制,从安全保障的角度来看,只相当于构建了可靠的硬件设施。
具体见P252
特权级1和2通常赋予那些可靠性不如内核的系统服务程序,比较典型的就是设备驱动程序当然,在很多比较流行的操作系统中,驱动程序与内核的特权级别相同,都是0。
- RPL:请求特权级
- DPL:描述符特权级
- CPL:当前特权级
DPL
每个描述符都有一个2bit的DPL字段(位于描述符中),对应的特权等级可以分别为0、1、2、3.

DPL总是指向他所描述的目标对象,DPL实际上是目标对象的特权级

CPL
当处理器在一个代码段中取指令和执行指令时,该代码段的特权级称为CPL当前特权级
操作系统从BIOS接受处理器控制权,并进入保护模式,肩负整个计算机系统的管理工作,必须工作在特权级0下。普通的 应用程序应该工作在特权级3下。
只有当前特权级CPL为0是才能执行的指令,称为特权指令(包括lgdt设置全局描述符表,lldt加载局部描述符表,ltr加载任务寄存器,mov和hlt等等)
处理器还对各特权级所能执行的IO操作进行控制,在EFLAGS中的12和13位代表IO特权级。处理器不限制0特权级的IO操作,但是可以限制低特权级程序的IO访问权限

RPL
引入RPL是处理器在面对选择子传送指令时无法判断真正的请求者是谁,当较低特权级的应用程序通过门调用访问数据段时,通过将服务例程的选择子RPL设置成应用程序的RPL特权级,防止应用程序访问高于RPL级别的数据段

特权保护

数据段的保护
对于数据段来说,只允许高特权级访问低特权级,也就是CPL<=数据段描述符DPL,但是为了方式低特权级通过门调用访问高特权级数据段,引入RPL进行检查,即
每当处理器执行一个段选择子传送指令时,会执行两项检查:
- 当前CPL高于或和数据段描述符的DPL相同,即数值上,CPL<=数据段描述符DPL
- 当前RPL高于或和数据段描述符的DPL相同,即数值上,RPL<=数据段描述符DPL
如果以上两个条件不能同时成立,处理器就会产生异常中断
引入RPL是处理器在面对选择子传送指令时无法判断真正的请求者是谁,当较低特权级的应用程序通过们调用访问数据段时,通过将服务例程的选择子RPL设置成应用程序的RPL特权级,防止应用程序访问高于RPL级别的数据段
代码段的保护
一般来说只允许想同级别的代码段访问和跳转,OS禁止高特权级代码段访问低特权级,但是可以通过以下方式让低特权级访问高特权级代码:
(1)将高特权级定义为依从代码
将控制权转移到依从的代码段,要求要求当前代码段CPL小于等于目标代码段描述符DPL,数值上,CPL>=数据段描述符DPL
(2)门调用
当前特权级CPL和请求特权级RPL高于,或者和调用门描述符特权级DPL相同。
数值上,CPL<=调用门描述符的DPL RPL<=调用门描述符的DPL
两个条件不能同时成立,则引发异常中断
☆☆☆☆特权检查规则
-
将控制权转移到依从的代码段,要求要求当前代码段CPL和RPL小于等于目标代码段描述符DPL,数值上:
CPL>=代码段描述符DPL
RPL>=代码段描述符DPL -
将控制权转移到非依从的代码段上,要求当前CPL和请求特权级RPL都等于目标代码段描述符DPL,数值上:
CPL=代码段描述符DPL
RPL=代码段描述符DPL
依从代码段,段描述符中的TYPE由C位,若C=0则代码段只能供同特权级的程序使用,如果C=1则成为依从代码段可以从特权级较低的程序调用并进入
还可以通过门调用在特权级代码段之间转移,使用 jmp far指令,可以将控制通过门转移到比当前特权级高的代码段,但不改变当前特权级别。但是,如果使用 call far指令,则当前特权级会提升到目标代码段的特权级别。也就是说,处理器是在目标代码段的特权级上执行的。但是,除了从高特权级别的例程(通常是操作系统例程)返回外,不允许从特权级高的代码段将控制转移到特权级低的代码段,因为操作系统不会引用可靠性比自己低的代码
-
高特权级别的程序可以访问低特权级别的数据段,但低特权级别的程序不能访问高特权级别的数据段。数值上:
CPL<=数据段描述符DPL
RPL<=数据段描述符DPL -
在任何时候,栈段的特权级别必须和当前特权级CPL相同。数值上:
CPL=目标栈段描述符DPL
RPL=目标栈段描述符DPL
调用门
将用户程序的特权级放为3,将用户程序作为任务加载,而不是将处理器交给用户程序,此时用户程序运行时的特权级别为3
调用门( Call-Gate)用于在不同特权级的程序之间进行控制转移。本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LDT中。下面是低32位,上面是高32位。
描述符中TYPE字段用于标示门的类型,共4bit,1100标示调用门
通过调用门实施特权级之间的控制转移时,可以使用 jmp far指令,也可以使用 call far指令如果是后者,会改变当前特权级CPL。因为栈段的特权级必须同当前特权级保持一致,因此,要切换栈,即,从低特权级的栈切换到高特权级的栈。
为了切换栈,每个任务除了自己固有的栈之外,还必须额外定义几套栈,具体数量取决于任务的特权级别。0特权级任务不需要额外的栈,它自己固有的栈就足够使用,因为除了调用返回外,不可能将控制转移到低特权级的段;1特权级的任务需要额外定义一个描述符特权级DPL为0的栈,以便将控制转移到0特权级时使用;2特权级的任务则需要额外定义两个栈,描述符特权级DPL分别是0和1,在控制转移到0特权级和1特权级时使用;3特权级的任务最多额外定义3个栈,描述符特权级分别是0、1和2,在控制转移到0、1和2特权级时使用。
这些额外创建的栈,其描述符位于任务自己的LDT中。同时,还要在任务的TSS中登记,原因是,栈切换是由处理器固件自动完成的,处理器需要根据TSS中的信息来完成这一过程。见上面的图。在TSS内,从偏移4~24处登记有特权级0到2的栈段选择子,以及相应的ESP初始值。任务自己固有的栈信息则位于偏移量为56(ESP)和80(SS)的地方。
任务寄存器TR总是指向当前任务的任务状态段TSS,其内容为该TSS的基地址和界限。在切换栈时,处理器可以用TR找到当前任务的TSS,并从TSS中获取新栈的信息通过调用门使用高特权级的例程服务时,调用者会传递一些参数给例程。如果是通过寄存器传送,这没有什么可说的。不过,要传递的参数很多时,更经常的做法是通过栈进行。调用者把参数压入栈,例程从栈中取出参数。
例程需要什么参数,先压入哪个参数,后压入哪个参数,这是调用者和例程之间的约定。特别是,当栈切换时,参数还在旧栈中。为了使例程能获得参数,必须将参数从旧栈复制到新栈中。参数的复制工作是由处理器固件完成的,但它必须事先知道参数的个数,并根据该数量决定复制多少内容。所以,调用门描述符中还有一个参数个数字段,共5比特。就是说,至多允许传送31个参数。
调用门描述符中的DPL和目标代码段描述符的DPL用于决定哪些特权级的程序可以访问此门。必须同时符合以下两个条件才行:
- 当前特权级CPL和请求特权级RPL高于,或者和调用门描述符特权级DPL相同。
数值上,CPL<=调用门描述符的DPL RPL<=调用门描述符的DPL - 当前特权级CPL低于,或者和目标代码段描述符特权级DPL相同。
数值上,CPL>=目标代码段描述符的DPL
![在这里插入图片描述]()
一个规定下限,一个规定上限
任务控制块
加载程序并创建一个任务,需要用到很多数据比如程序的大小、加载的位置,等等。当任务执行结束,还要依据这些信息来回收它所占用的内存空间(在本书中没有体现,但一个合格的操作系统必须实现该功能)。多任务系统是多个任务同时运行的,特别是在一个单处理器(核)的系统中,为了在任务之间切换和轮转,必须能追踪到所有正在运行的任务,记录它们的状态,或者根据它们的当前状态来采取适当的操作

为了满足以上的要求,内核应当为每一个任务创建一个内存为了能够追踪到所有任务,应当把每个任务控制块TCB串起来,形成一个链表区域,来记录任务的信息和状态,称为任务控制块( Task Control Block,TCB)。
任务在运行时,需要调用内核或者操作系统的例程。这可以认为是从同一个任务的局部地址空间转移到全局地址空间工作。而且,在这个过程中涉及特权级的变化,需要通过调用门通过调用门的控制转移通常会改变当前特权级CPL,同时还要切换到与目标代码段特权级相同的栈。
必须为每个任务定义额外的栈。对于当前的3特权级任务来说,应当创建特权级0、1和2的栈。而且,应当将它们定义在每个任务自己的LDT中。
全局描述符表(GDT)是唯一的,整个系统中只有一个,所以只需要用GDTR寄存器存放其线性基地址和段界限即可;但LDT不同,每个任务一个,所以,为了追踪它们,处理器要求在GDT中安装每个LDT的描述符。当要使用这些LDT时,可以用它们的选择子来访问GDT,将LDT描述符加载到LDTR寄存器。

TSS(任务状态段)内偏移0处是前一个任务的TSS描述符选择子。和LDT一样,必须在全局描述符表(GDT)中创建每个TSS的描述符。当系统中有多个任务同时存在时,可以从一个任务切换到另一个任务执行,此时称任务是嵌套的。被嵌套的任务用这个指针指向前一个任务,即嵌套它的那个任务,当控制返回前一个任务时,处理器需要这个指针来识别前一个任务。创建TSS时,可以为0。

TSS描述符中的B位是“忙”位(Busy)。在任务刚刚创建的时候,它应该为二进制的1001,即,B位是0,表明任务不忙。当任务开始执行时,或者处于挂起状态(临时被中断执行)时,由处理器固件把B位置1。
任务不可重入,B位由处理器固件管理

当使用 jmp far指令通过调用门转移控制时,要求当前特权级和目标代码段的特权级相同。原因是用 jmp far指令通过调用门转移控制时,不改变当前特权级CPL。
相反,使用 call far指令可以通过调用门将控制转移到较高特权级别的代码段。之所以说“可以”,是因为,如果目标代码段是依从的,则和 jmp far指令一样,不改变当前特权级别;否则,如果目标代码段是非依从的,则在目标代码段的特权级别上执行。

TR和LDTR寄存器都包括16位的选择器部分,以及的描述符高速缓存器部分。选择器部分的内容是TR和LDT描述符的选择子;描述符高速缓存器部分的内容则指向当前任务的TSS和LDT,以加速这两个段(表)的访问。

任务切换
从80286开始的处理器是面向多任务系统而设计的。在一个多任务的环境中,可以同时存在多个任务,每个任务都有各自的局部描述符表(LDT)和任务状态段(TSS)。在局部描述符表中存放着专属于任务局部空间的段的描述符。可以在多个任务之间切换,使它们轮流执行,从一个任务切换到另一个任务时,具体的切换过程是由处理器固件负责进行的。
所谓多任务系统,是指能够同时执行两个以上任务的系统。即使前一个任务没有执行完,下个任务也可以开始执行。但是,什么时候切换到另一个任务,以及切换到哪一个任务执行,主要是操作系统的责任,处理器只负责具体的切换过程,包括保护前一个任务的现场。
有两种基本的任务切换方式,一种是协同式的,从一个任务切换到另一个任务,需要当前任务主动地请求暂时放弃执行权,或者在通过调用门请求操作系统服务时,由操作系统“趁机”将控制转移到另一个任务。这种方式依赖于每个任务的“自律”性,当一个任务失控时,其他任务可能得不到执行的机会。
另一种是抢占式的,在这种方式下,可以安装一个定时器中断,并在中断服务程序中实施任务切换。硬件中断信号总会定时出现,不管处理器当时在做什么,中断都会适时地发生,而任务切换也就能够顺利进行。在这种情况下,每个任务都能获得平等的执行机会。而且,即使一个任务失控,也不会导致其他任务没有机会执行。

任务切换是以任务为单位的,是指离开一个任务,转到另一个任务中去执行。任务转移相对来说要复杂得多,当一个任务正在执行时,处理器的各个部分都和该任务息息相关:段寄存器指向该任务所使用的内存段;通用寄存器保存着该任务的中间结果,等等。离开当前任务,转到另一个任务开始执行时,要保存旧任务的各种状态,并恢复新任务的运行环境。
处理器在刚进入保护模式时,是以0特权级别运行的,而且执行的一般是操作系统代码,也必须是0特权级别的,这样才能方便地控制整个计算机。其次,任务并不一定非得是3特权级别的,也可以是0特权级别的。特别是,操作系统除了为每一个任务提供服务外,也会有一个作为任务而独立存在的部分,而且是0特权级别的任务,以完成一些管理和控制功能,比如提供一个界面和用户进行交互。
任务切换方法
第一种任务切换的方法是借助于中断,这也是现代抢占式多任务的基础。原因很简单,只要中断没有被屏蔽,它就能随时发生。特别是定时器中断,能够以准确的时间间隔发生,可以用来强制实施任务切换。毕竟,没有哪个任务愿意交出处理器控制权,也没有哪个任务能精确地把握交出控制权的时机。
在实模式下,内存最低地址端的1KB是中断向量表,保存着256个中断处理过程的段地址和偏移地址。当中断发生时,处理器把中断号乘以4,作为表内索引号访问中断向量表,
在保护模式下,中断向量表不再使用,取而代之的,是中断描述符表。它和GDT、LDT是一样的,用于保存描述符。唯一不同的地方是,它保存的是门描述符,包括中断门、陷阱门和任务门。这些门和调用门是非常类似的。当中断发生时,处理器用中断号乘以8(因为每个描述符占8字节),作为索引访问中断描述符表,取出门描述符。门描述符中有中断处理过程的代码段选择子和段内偏移量,这和调用门是一样的。
一般的中断处理可以使用中断门和陷阱门。调用门的工作原理,只是从任务的局部空间转移到更高特权级的全局空间去执行,本质上是一种任务内的控制转移行为。与此相同中断门和陷阱门允许在任务内实施中断处理,转到全局空间去执行一些系统级的管理工作,本质上,也是任务内的控制转移行为
但是,在中断发生时,如果该中断号对应的门是任务门,那么,性质就截然不同了,必须进行任务切换。即,要中断当前任务的执行,保护当前任务的现场,并转换到另一个任务去执行。

任务门描述符中的主要成份是任务的TSS选择子。任务门用于在中断发生时执行任务切换,而执行任务切换时必须找到新任务的任务状态段(TSS)。所以,任务门应当指向任务的TSS。为了指向任务的TSS,只需要在任务门描述符中给出任务的TSS选择子就可以了。
当中断发生时,处理器用中断号乘以8作为索引访问中断描述符表。当它发现这是个任务门(描述符)时,就知道应当发起任务切换。于是,它取出任务门描述符;再从任务门描述符中取出新任务的TSS选择子;接着,再用TSS选择子访问GDT,取出新任务的TSS描述符。在转到新任务执行前,处理器要先把当前任务的状态保存起来。当前任务的TSS是由任务寄存器TR的当前内容指向的,所以,处理器把每个寄存器的“快照”保存到由TR指向的TSS中。然后,处理器访问新任务的TSS,从中恢复各个寄存器的内容,包括通用寄存器、标志寄存器EFLAGS、段寄存器、指令指针寄存器EIP、栈指针寄存器ESP,以及局部描述符表寄存器(LDTR)等。最终,任务寄存器TR指向新任务的TSS,而处理器旋即开始执行新的任务。一旦新任务开始执行,处理器固件会自动将其TSS描述符的B位置“1”,表示该任务的状态为忙
当中断发生时,可以执行常规的中断处理过程,也可以进行任务切换。尽管性质不同,但它们都要使用iret指令返回。前者是返回到同一任务内的不同代码段;后者是返回到被中断的那个任务。问题是,处理器如何区分这两种截然不同的返回类型呢?32位处理器的 EFLAGS有NT位(位14),意思是嵌套任务标志( Nested Task Flag)。每个任务的TSS中都有一个任务链接域(指向前一个任务的指针,参见TSS的结构),可以填写为前一个任务的TSS描述符选择子。如果当前任务 EFLAGS寄存器的NT位是“1”,则表示当前正在执行的任务嵌套于其他任务内,并且能够通过TSS任务链接域的指针返回到前一个任务。
可以使用iret指令从当前任务返回(转换)到前一个任务,前提是当前任务 EFLAGS寄存器的NT位必须是“1”。无论任何时候处理器碰到iret指令,它都要检查NT位,如果此位是0,表明是一般的中断过程,按一般的中断返回处理,即,中断返回是任务内的(中断处理过程虽然属于操作系统,但属于任务的全局空间);如果此位是1,则表明当前任务之所以能够正在执行,是因为中断了别的任务。因此,应当返回原先被中断的任务继续执行。此时,由处理器固件把当前任务 EFLAGS寄存器的NT位改成“0”,并把TSS描述符的B位改成“0”(非忙)。在保存了当前任务的状态之后,接着,用新任务(被中断的任务)的TsS恢复现场。
除了因中断引发的任务切换之外,还可以用远过程调用指令CALL,或者远跳转指令JMP直接发起任务切换。在这两种情况下,CALL和JMP指令的操作数是任务的TSS描述符选择子或任务门。以下是两个例子
call 0x0010:0x00000000 jmp 0x0010:0x00000000
当处理器执行这两条指令时,首先用指令中给出的描述符选择子访问GDT,分析它的描述符类型。如果是一般的代码段描述符,就按普通的段间转移规则执行;如果是调用门,按调用门的规则执行;如果是TSS描述符,或者任务门,则执行任务切换。此时,指令中给出的32位偏移量被忽略,原因是执行任务切换时,所有处理器的状态都可以从TSS中获得(包括用户程序的入口)。注意,任务门描述符可以安装在中断描述符表中,也可以安装在全局描述符表(GDT)或者局部描述符表(LDT)中。(本书中安装在GDT中)
意味着只要调用任务门,调用TSS选择子就能切换任务,任务的入口,ldt和寄存器信息都已经包括在了TSS中
如果是用于发起任务切换,ca指令和jp指令也有不同之处。使用call指令发起的任务切换类似于因中断发起的任务切换。这就是说,由cal指令发起的任务切换是嵌套的,当前任务(旧任务)TSS描述符的B位保持原来的“1”不变, EFLAGS寄存器的NT位也不发生变化;新任务TSS描述符的B位置“1”, EFLAGS寄存器的NT位也置“1”,表示此任务嵌套于其他任务中。同时,TSS任务链接域的内容改为旧任务的TSS描述符选择子。用CALL指令发起的任务切换,可以通过iret指令返回到前一个任务。此时,旧任务TSS描述符的B位,以及 EFLAGS寄存器的NT位都恢复到“0”。

和call指令不同,使用jmp指令发起的任务切换,不会形成任务之间的嵌套关系。执行任务切换时,当前任务(旧任务)TSS描述符的B位清零,变为非忙状态, EFLAGS寄存器的NT位不变;新任务TSS描述符的B位置“1”,进入忙的状态, EFLAGS寄存器的NT位保持从TSS中加载时的状态不变。
处理器用以下四种方法将控制转换到其他任务:
- 当前程序、任务或者过程执行一个将控制转移到GDT内某个TSS描述符的jmp或者call指令;
- 当前程序、任务或者过程执行一个将控制转移到GDT或者当前LDT内某个任务门描述符的jmp或者call指令
- 一个异常或者中断发生时,中断号指向中断描述表内的任务门;
- 在 EFLAGS寄存器的NT位置位的情况下,当前任务执行了一个iret指令。
参考
x86汇编语言:从实模式到保护模式,第14章


浙公网安备 33010602011771号