任务全景图
任务,任务的LDT和TSS
程序是记录在载体上的指令和数据,其正在执行中的一个副本,叫做任务(Task)。而用户程序就是任务,内核程序就是操作系统的缩影。
LDT
我们把所有的段描述符都放在GDT中。为了有效的在任务之间实施隔离,处理器建议每个任务都应该具有自己的描述符表LDT,并且把专属自己的那些段放到LDT中。
和GDT一样,LDT也是用来存放描述符的。不同之处在于,LDT只属于某个任务。或者说,每个任务都有自己的LDT,每个任务私有的段,都应当在LDT中进行描述。另外,LDT的第一个描述符(0号槽位)是有效的,可以使用的。
和GDT不同,局部描述符表(LDT)的数量则不止一个,具体有多少,视任务的多少而定。为了追踪和访问这些LDT,处理器使用了局部描述符表寄存器LDTR。
因为LDTR寄存器只有一个,所以它只用于指向当前任务的LDT。每当发生任务切换时,LDTR的内容被更新,以指向新任务的LDT。
在引用一个段时,需要给出段选择子,选择子的位2(TI位)是表指示器,若TI = 0,表示从GDT中加载描述符,TI = 1则从当前任务的LDT中加载描述符。很显然, 应为段选择子是16位的,而且只有高13位被用做索引号来访问GDT或者LDT,所以,每个LDT所能容纳的描述符个数为2^13,即8192个。又因每个描述符8字节,LDT的长度最大为64KB。
TSS
为了保存任务的状态,并在下次重新执行时恢复他们,每个任务都应当用一个额外的内存区域保存相关信息,这叫做任务状态段。
处理器将当前任务的现场信息保存到由TR寄存器指向的TSS,然后,再使TR寄存器只相信任务的TSS,并在任务切换的时候读取其中的信息。
任务段TSS具有固定的格式,最小尺寸是104字节。处理器固件能够时别TSS中的每个元素,并在任务切换的时候读取其中的信息。
SS0,SS1和SS2分别是0,1和2特权级的栈段选择子,ESP0,ESP1和ESP2分别是0,1和2特权级的栈顶指针。这些内容应当由任务的创建者填写,且属于填写后一般不变的静态部分,当通过门进行特权级之间的控制转移时,处理器用这些信息来切换栈。
CR3和分页有关,此处一般由任务的创建者填写,如果没有使用分页,可以为0。
偏移为32~92的区域是处理器寄存器的快照部分,用于在进行任务切换时,保存处理器的状态以便将来恢复现场。在一个多任务环境中,每次创建一个任务时,操作系统或者内核至少要填写EIP,EFLAGS,ESP,CS,SS,DS,ES,FS和GS,当该任务第一次获得执行时,处理器从这里加载初始化执行环境,并从CS:EIP处开始执行任务的第一条指令。在此之后的任务运行期间,该区域的内容由处理器固件进行更改。
LDT段选择子是当前任务的LDT描述符选择子。由内核或者操作系统填写,以指向当前任务的LDT。该信息由处理器在任务切换时使用,在任务运行期间保持不变。
T位用于软件调试。在多任务的环境中,如果T位是1,每次切换到该任务时,将引发一个调试异常中断。
I/O映射基地址用于决定当前任务是否可以访问特定的硬件端口。
每个任务都有EFLAGS寄存器的副本,其内容在任务创建的时候由内核或者操作系统初始化,在多任务系统中,每次当任务恢复运行时,就由处理器固件自动从TSS中恢复。EFLAGS寄存器的IOPL位决定了当前任务的I/O特权级别。如果当前特权级CPL高于,或者和任务的I/O特权级IOPL相同时。所有I/O操作都是允许的,针对任何硬件端口的访问都可以通过。但是若是当前特权级CPL低于IOPL则还要看I/O许可位。
I/O许可位串是一个比特序列,或者说是一个比特串,最多允许65536比特(处理器可以访问65536个硬件端口),即8KB。从第1比特开始,各比特用它在串中的位置代表一个端口号,因此,第一个比特代表0号端口,第2个比特代表1号端口…….第65536比特代表第65535号端口。每个比特的取值决定了相应的端口是否允许访问。为1时,禁止访问;为0时,允许访问。处理器检查I/O许可位的方法是先计算它在I/O许可位映射区的字节编号,并读取该字节,然后进行测试。
TSS最小的大小为104,但是其最大的大小根据其I/O许可为串来决定。
LDTR和TR
LDTR指向新任务的LDT(LDT与GDT格式类似),而TR指向任务的TSS(如上图)。
LDT和TSS需要安装在GDT中,以描述符的形式存在
LDT:全局描述符GDT是唯一的,整个系统只有一个,所以只需要GDTR寄存器村放其线性地址和段界限即可;但LDT不同,每个任务一个,所以,为了追踪它们,处理器要求在GDT中安装每个LDT的描述符。当要使用这些LDT时,可以用它们的选择子来访问GDT,如果不这样, 处理器将没有机会来做存储器和特权级的保护工作。
TSS :必须在GDT中安装TSS的描述符。这样做,一方面是为了对TSS进行段和特权级的检查;另一方面,也是执行任务切换的需要。当call far和jmp far指令的操作数是TSS描述符选择子时,处理器执行任务切换操作。
LDT和TSS描述符的格式差不多,除了TYPE位(LDT的TYPE位为0010二进制表示这个描述符是LDT段描述符,TSS的TYPE位为10B1)。其中TSS是忙位,B为0,表示任务不忙,B为1,表示任务正在执行或者处于挂起状态。
AVL:表示可用和保留位,供系统软件使用。
B位在上面已经说了。
Base:基地址字段,该字段定义在4GB线性地址空间中一个段字节0所处的位置。
DPL:描述符特权级字段,用于指明描述符的特权级。特权级范围从0到3。0级特权级最高,3级最低。DPL用于控制对段的访问。
G:颗粒度标志,该字段用于确定段限长字段Limit值的单位。如果颗粒度标志为0,则段限长值的单位是字节;如果设置了颗粒度标志,则段限长值使用4KB单位。
LIMIT:段限长字段,用于指定段的长度。
P:段存在标志,用于指出一个段是在内存中(P=1)还是不在内存中(P=0)。
TYPE:段类型字段,用行指定段或门(Gate)的类型、说明段的访问种类以及段的扩展方向。
全局空间和局部空间
每个任务实际包括两个部分:全局部分和私有部分。全局部分是所有任务共有的,含有操作系统的软件和库程序,以及可以调用的系统服务和数据;私有部分则是每个任务各自的数据和代码,与任务所要解决的具体问题有关,彼此并不相同。
实际上各个任务不可相见,他们维护了各自的LDT,所以也就完成了任务的隔离。
地址空间的访问是依靠分段机制来进行的。因此,全局地址空间是用全局描述符表(GDT)来指定的,而局部地址空间则是由每个任务私有的局部描述符表(LDT)来指定的。
对我们来说,任务的全局空间包含了操作系统的段,由别人编写,但是我们可以调用这些段的代码,或者获取这些段的数据,任务局部空间的内容由我们编写。通常,任务会在自己的局部空间运行,当它需要操作系统提供服务时,转入全局空间执行。
特权级保护
特权级也叫特权级别,是存在于描述符及其选择子中的一个数值,当这些描述符或者选择子所指向的对象要进行某种操作,或者被别的对象访问时,该数值用于控制他们所能进行的操作,或者限制他们的可访问性。
Intel处理器可以识别4种特权级别,分别是0到3,较大的数值意味着较低的特权级别,反之亦然。
每个描述符都由一个2位的DPL字段,可以取值0~3。DPL是每个描述符都有的字段,故又称描述符特权级。描述符总是指向它描述的目标对象,代表着该对象,因此,该字段实际上是目标对象的特权级。
对于数据段来说,DPL决定了访问它们所应当具备的最低特权级别。如果一个数据段,其描述符的DPL字段为2,那么,只有特权级位0,1,2的程序才能访问它。当特权级3的程序试图去读写该段时,将会被处理器阻止,并引发异常中断。
实际上任务就是被分成了特权级截然不同的两个部分,全局部分是特权级0的,而局部空间则是特权级3的。
只有在当前特权级CPL为0时才能执行的指令,称为特权指令。除了那些特权级敏感的程序,处理器还允许对各个特权级别所能执行的I/O操作进行控制。通常,这指的是端口访问的许可权,因为对设备的访问都是通过端口进行的。在处理器的标志寄存器EFLAGS中, 位13, 位12是IOPL位, 也就是输入/输出特权级(IOPL), 它代表着当前任务的I/O特权级别。
特权级不同的转移方式
代码段的特权级检查是很严格的。一般来说,控制转移只允许发生在两个特权级相同的代码段之间。不过,为了让特权级低的应用程序可以调用特权级高的操作系统历程,处理器也提供了相应的解决办法:
- 将高特权级的代码段定义为依从的。代码段描述符的TYPE字段有C位,如果C = 0,这样的代码段只能供同特权级的程序使用,如果C = 1,这样的代码段称为依从代码段,可以从特权级比它低的程序调用并进入。但是,要求当前特权级CPL必须低于, 或者和目标代码段描述符的DPL相同。即在数值上:
CPL >= 目标代码段描述符的DPL
在任何时候,都不允许将控制从较高的特权级转移到较低的特权级。依从的的代码段不是在它的DPL特权级上运行的,而是在调用程序的特权级上运行的。当控制转移到依从的代码段上执行时,不改变当前特权级CPL,被调用过程的特权级依从与调用者的特权级, 这就是为什么它被称为”依从的”代码段.
- 另一种在特权级之间转移控制的方法是使用门。门(Gate)是另一种形式的描述符,称为门描述符,简称门。根据不同的用途,门的类型有好几种。不同特权级之间的过程调用可以用调用门。中断门/陷阱门是作为中断处理过程使用的;任务门对应着单个的任务,用来执行任务切换。所有描述符都是64位的,门描述符也不例外。在调用门描述符中,定义了目标过程所在代码段的选择子,以及段内偏移。
要想通过调用门进行控制转移,可以使用jmp far后者call far指。
使用jmp far指令,可以将控制通过门转移到比当前特权级高的代码段,但不改变当前特权级。
使用call far指令,则当前特权级会提升到目标代码段的特权级别。也就是说处理器是在目标代码段的特权级上执行的。
除了从高特权级别的历程(通常是操作系统历程)返回外,不允许从特权级高的代码段将控制转移到特权级低的代码段。
调用门
调用门用于在不同特权级的程序之间进行控制转移。本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LDT中。
调用门描述符给出了例程所在代码段的选择子,而不是32位线性地址。有了就能访问描述符表得到代码段的基地址,这样做无非是间接了一点,但却可以在通过调用门进行控制转移时,实施代码段描述符有效性,段界限和特权级的检查。
描述符的TYPE字段用于标识门的类型,共4位,值1100标识调用门。
P位是有效位,通常应该是1,当它为0时,调用这样的门会导致处理器产生异常中断。
通过调用门实施特权级转移时,可以用jmp far,也可以用call far指令。如果是后者。会改变当前特权级CPL。因为栈段的特权级必须同当前特权级保持一致,因此,还要切换栈。
为了切换栈,每个任务除了自己固有的栈之外,还必须额外定义几个栈,具体数量取决于任务的特权级别。0特权级任务不需要额外的栈,它自己固有栈就足够使用,因外除了调用返回外,不可能将控制转移到低特权级的段;1特权级的任务需要额外定义一个描述符特权级DPL为0的栈,以便将控制转移到0特权级时使用;2特权的任务需要额外定义描述符特权级DPL为0和1的栈,在控制转移到0和1特权级时使用;3特权的任务需要额外定义描述符特权级DPL为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
举个例子,如果调用门描述符的DPL为2,那么,只有特权级为0,1和2的程序才允许使用该调用门,特权级3的程序使用此门时将引发异常中断。
借助call far调用门,当程序从的执行流从低特权级转入高特权级的代码段时,如果那是个非依从的代码段,当前特权级也随之变为目标代码段的特权级。不过,如果调用者和被调用者的特权级相同,则特权级不会变化。所以,在控制转移的过程中也不会发生栈切换,仅仅是把返回地址CS和EIP压入当前栈。当执行retf指令后,处理器从栈中恢复CS和EIP的原始内容,于是又返回到原先的代码段接着执行。
而借助jmp far调用门,只用于不需要从调用门返回的场合下,而且不改变当前特权级。也就是说,目标代码是在当前特权级上执行。
调用门转移控制的完整过程
指令 | 特权级检查规则 |
---|---|
CALL FAR | CPL<=调用门描述符的DPL,RPL<=调用门描述符的DPL |
对于依从和非依从代码段:CPL>=目标代码段描述符的DPL | |
JMP FAR | CPL<=调用门描述符的DPL;RPL<=调用门描述符的DPL |
对于依从代码段要求CPL>=目标代码段描述符的DPL | |
对于非依从代码段只要求CPL=目标代码段描述符的DPL |
当使用call far指令通过调用门转移控制时,如果改变了当前的特权级别,则必须切换栈。即,从当前任务的固有栈切换到与目标代码段特权级别相同的栈上,栈的切换是由处理器固件自动进行的。
当前栈是由段寄存器SS和栈指针寄存器ESP的当前内容指示的;要切换的新栈位于当前任务的TSS中,处理器知道如何找到它。在栈切换前,处理器要检查新栈是否有足够的空间完成本次控制转移。
栈切换过程如下:
使用目标代码段的DPL(也就是新的CPL)到当前任务的TSS中选择一个栈,包括栈段选择子和栈指针。
从TSS中读取所选择的段选择子和栈指针,并用该选择子读取栈段描述符。在此期间,任何违反段界限检查的行为都将引发处理器异常中断(无效TSS)。
检查栈段描述符的特权级和类型,并可能引发处理器异常中断(无效TSS)。
临时保存当前栈段寄存器SS和栈指针ESP的内容。
把新的栈段选择子和栈指针带入SS和ESP寄存器,切换到新栈。
将刚才临时保存的SS和ESP的内容压入当前栈。
根据调用门描述符“参数个数”字段的指示,从旧栈中将所有参数都复制到新栈中。
将当前段寄存器CS和指令指针寄存器EIP的内容压入新栈。
从调用门描述符中依次将目标代码段选择子和段内偏移传送到CS和EIP寄存器,开始执行被调用过程。
相反,如果没有该变特权级别,则不切换栈,继续使用调用者的当前栈,只在原来的基础上压入当前段寄存器CS和指令指针寄存器ESP的内容。
如果调用门的控制转移是jmp far指令发起的,结果就是肉包子打狗,有去无会。而且,没有特权级的变化,也不需要切换栈。而是用call far指令发起的,那么可以使用远返回指令retf把控制返回到调用者。
从同一特权级返回时,处理器将从栈中弹出调用者的代码段选择子和指令指针。尽管他们通常是有效的,但是,为了安全起见,处理器依然会进行特权级检查。
要求特权级变换的远返回,只能返回到较低的特权级别上。
控制返回的全部过程如下:
检查栈中保存的CS寄存器的内容,根据其RPL字段决定返回时是否需要改变特权级别。
从当前栈中读取CS和EIP寄存器的内容,并针对代码段描述符和代码段选择子的RPL字段实施特权级检查。
如果远返回指令是带参数的,则将参数和ESP寄存器的当前值相加,以跳过栈中的参数部分。 最后的结果是ESP寄存器指向调用者SS和ESP的压栈值。注意,retf指令的字节计数值必须等于调用门中的参数乘以参数长度。
如果返回时需要改变特权级,从栈中将SS和ESP的压栈值代入段寄存器SS和指令指针寄存器ESP,切换到调用者的栈。在此期间,一旦检测到有任何界限违例的情况将引发处理器异常中断。
如果远返回指令是带参数的,则将参数和ESP寄存器的当前值相加,以跳过调用者栈中的参数部分。最后的结果是调用者的栈恢复到平衡位置。
如果返回时需要改变特权级,检查DS,ES,FS和GS寄存器的内容,根据它们找到相应的段描述符。要是有任何一个段描述符的DPL高于调用者的特权级(返回后的新CPL),即,在数值上:
段描述符的DPL<返回后的新CPL
基本的特权级检查规则
每当处理器执行一个将段选择子传送到段寄存器的指令时,会检查以下两个条件是否都能满足:
当前特权级CPL高于或者和数据段描述符的DPL相同。即,在数值上,CPL <= 数据段描述符的DPL
请求特权级RPL高于或者和数据段描述符的DPL相同。即,在数值上,RPL <= 数据段描述符的DPL
如果以上条件不能同时成立,处理器就会阻止这种操作,并引发异常中断。
将控制直接转移到非依从的代码段(因为特权级相等,转以后特权级不变),要求当前特权级CPL和请求特权级RPL都等于目标代码段描述符的DPL。即,在数值上:
CPL = 目标代码段描述符的DPL
RPL = 目标代码但描述符的DPL将控制直接转移到依从的代码段(转以后特权级不变),要求当前特权级CPL和请求特权级RPL都低于,或者和目标代码段描述符的DPL相同。即,在数值上:
CPL >= 目标代码段描述符的DPL
RPL >= 目标代码段描述符的DPL高特权级别的程序可以访问低特权级别的数据段,但低特权级别的程序不能访问高特权级别的数据段。访问数据段之前,肯定要对段寄存器进行修改,在这个时候,要求当前特权级CPL和请求特权级RPL都必须高于,或者和目标数据段描述符的DPL相同。即,在数值上:
CPL <= 目标数据段描述符的DPL
RPL <= 目标数据段描述符的DPL处理器要求,在任何时候,栈段的特权级别必须和当前特权级别CPL相同。因此,随着程序的执行,要对段寄存器SS的内容进行修改时,必须进行特权级检查。在对段寄存器SS修改时。要求当前特权级CPL和请求特权级RPL必须等于目标栈段描述符的DPL。即,在数值上:
CPL == 目标栈段描述符的DPL
RPL == 目标栈段描述符的DPL
0特权级是最高级别,当一个系统的各个部分位于0特权级别是,各种特权级别检查总能够获得通过,就像这种检查和检验并不存在一样。
小结
程序员在写程序时,不需要指定特权级别。当程序运行时,操作系统将程序创建为局部空间内容,并赋予较低的特权级别,比如3,操作系统对应任务全局空间的内容。如果有多个任务,则操作系统属于所有任务的公共部分。
当任务运行在局部空间时,可以在各个段之间转换控制,并访问私有数据,因为它们具有相同的特权级别,但不允许直接将控制转移到高特权级别的全局空间的段,除非通过调用门,或者目标段依从的代码段。
当通过调用门进入全局空间执行时,操作系统可以在全局空间内的各个段之间转移控制并访问数据,因为它们也具有相同的特权级别。同时,操作系统还可以访问任务局部空间的数据,即低特权级别的数据段。但除了调用门返回外,不允许将控制转移到低特权级别的局部空间内的代码段。
任何时候,当前栈的特权级别必须和CPL是一样的。进入不同特权级别的段执行时,要切换栈。
检查调用者的请求特权级RPL
一般来说,用户程序会提供一个RPL为3的段选择子给操作系统例程。通过调用门实施控制转移后,当前特权级CPL变成0,实际的请求者是用户程序,选择子的请求特权级RPL为3,要访问的段属于用户程序,其描述符的DPL为3,在数值上符合CPL <= DPL,并且RPL <= DPL的条件,可以正常执行。
RPL只是在原来的基础上多增加了一种检查机制,并把如何能够通过这种检查的自由裁量权交给软件(的编写者);
引入请求特权级的RPL的原因是处理器在遇到一条将选择子传送到段寄存器的指令时,无法区分真正的请求者是谁。但是,引入RPL本身并不能完全解决这个问题,这只是处理器和操作系统之间的一种协议,处理器负责检查请求特权级RPL,判断他是否有权访问,但前提是提供了正确的RPL,内核或者操作系统负责鉴别请求者的身份,并有义务保证RPL的值和它的请求者身份相符,因为这是处理器无能为力的。