在上一章,内核赋予用户程序的特权级别是0,所以用户程序是在0特权级上运行的。也正是因为如此,当用户程序通过U-SALT表中的符号地址直接调用内核例程时,才会通过特权级检查。在本章中,内核也做同样的工作。不同之处在于,它将用户程序的特权级定为3,也就是最低特权级别。没有人愿意将自己的程序放在特权级3上,但系统核心一定会将它放在特权级3上。
尽管保护模式非常复杂,但这并没有加重用户程序(应用程序)编写者(程序员)的负担,因为他们不必考虑底层的很多东西,这也是为什么本章没有提供用户程序代码清单的原因。事实上,本章将继续沿用第13章的用户程序,只不过要作为一个任务进行加载,加载的方法和上一章是不同的。而且,运行时的特权级别是3,不再是上一章中的0。
为了方便应用程序的编写,内核通常要提供大量的例程供它们调用。例如,在第13章中,用户程序可以调用内核例程@PrintString和@ReadDiskData。为此,用户程序需要定义SALT表,并在表中填写例程的符号名。之后,再由内核将符号名转换成入口地址,也就是该例程所对应的段选择子和段内偏移量。例程是由内核提供的,它们的特权级通常就是内核的特权级。
在上一章里,内核程序和用户程序都运行在0特权级,而且都是普通的段间控制转移,所以,在用户程序直接调用内核例程,这不会有任何问题。但在本章中,用户程序运行时的特权级别将会是3.由于处理器禁止将控制从特权级低的程序转移到特权级高的程序,因此,如果还像以前那样直接调用内核例程,百分之百不会成功,一定会引发处理器异常中断。但是,现实的需求也不能不予考虑,任何操作系统都应当提供大量的功能调用服务。为此,需要安装调用门。
调用门(Call-Gate)用于在不同特权级的程序之间进行控制转移。本质上,它只是一个描述符,一个不同于代码段和数据段的描述符,可以安装在GDT或者LDT中。该描述符的格式如图14-010所示,下面是低32位,上面是高32位。
如上图,调用门描述符给出了例程所在代码段的选择子,而不是32位线性地址。有了段选择子,就能访问描述符表得到代码段的基地址,这样做无非是间接了一点,但却可以在通过调用门进行控制转移时,实施代码段描述符有效性、段界限和特权级的检查。例程在代码段中的偏移量也是在描述符中直接指定的,只是被分成了两个16位的部分。很显然,在通过调用门调用例程时,不使用指令中给出的偏移量。
描述符中的TYPE字段用于标识门的类型,共4比特,值“1100”标识调用门。P位是有效位,通常应该是“1”。当它为“0”时,调用这样的门会导致处理器产生异常中断。对于操作系统来说,这个机关可能会很有用。比如,为了统计调用门的使用频率,可以将它置“0”。然后,每当因调用该门而产生异常中断时,它们属于故障中断,从中断处理过程返回时,处理器还会重新执行引起故障的指令。此时,因P已经为“1”,所以可以执行。就当前的例子而言,因为在提供调用门服务的同时,还要统计门的调用次数,故,可以在该调用门所对应的例程中将P位清零。这样,下一次该门被调用时,又会重复以上过程。
通过调用门实施特权级之间的控制转移时,可以使用jmp far指令,也可以使用call far指令。如果是后者,会改变当前特权级CPL。因为栈段的特权级必须同当前特权级保持一致,因此,还要切换栈,即,从低特权级的栈切换到高特权级的栈。比如,一个特权级为3的程序必须使用自己的3特权级栈工作。当它通过调用门进入0特权级的代码段执行时,当前特权级由3变为0。此时,栈也要跟着切换,从3特权级的栈切换到0特权级的栈。这主要是为了防止因栈空间不足而产生不可预料的问题,同时也是为了防止栈数据的交叉引用。
为了切换栈,每个任务除了自己固有的栈之外,还必须额外定义几套栈,具体数量取决于任务的特权级别。0特权级任务不需要额外的栈,它自己固有的栈就足够使用,因为除了调用返回外,不可能将控制转移到低特权级的段;1特权级的任务需要额外定义一个描述符特权级DPL为0的栈,以便将控制转移到0特权级时使用;2特权级的任务则需要额外定义两个栈,描述符特权级DPL分别是0和1,在控制转移到0特权级和1特权级时使用;3特权级的任务最多额外定义3个栈,描述符特权级分别是0、1和2,在控制转移到0、1和2特权级时使用。
这些额外创建的栈,其描述符位于任务自己的LDT。同时,还要在任务的TSS中登记,原因是,栈切换是由处理器固件自动完成的,处理器需要根据TSS中的信息来完成这一过程。图14-001中,在TSS内,从偏移4~24处登记有特权级0到2的栈段选择子,以及相应的ESP初始值。任务自己固有的栈信息则位于偏移量为56(ESP)和80(SS)的地方。
任务寄存器TR总是指向当前任务的任务状态段TSS,其内容为该TSS的基地址和界限。在切换栈时,处理器可以用TR找到当前的TSS,并从TSS中获取新栈的信息。
通过调用门使用高特权级的例程服务时,调用者会传递一些参数给例程。如果是通过寄存器传送,这没有什么可说的,不过,要传递的参数很多时,更经常的做法是通过栈进行。调用者把参数压入栈,例程从栈中取出参数。在高级语言里,这是一贯的做法。
例程需要什么参数,先压入哪个参数,后压入哪个参数,这是调用者和例程之间的约定,调用者是清楚的。否则,它不会调用这个例程。但是,这一切对于处理器来说是懵懂的。特别是,当栈切换时,参数还在旧栈中。为了使例程能获得参数,必须将参数从旧栈复制到新栈中。
参数的复制工作是由处理器固件完成的,但它必须事先知道参数的个数,并根据该数量决定复制多少内容。所以,调用门描述符中还有一个参数个数字段,共5比特。就是说,至多允许传送31个参数。
栈切换前,段寄存器SS指向的是旧栈,ESP指向旧栈的栈顶,即最后一个被压入的过程参数;栈切换后,处理器自动替换SS和ESP寄存器的内容,使它们分别为新栈的选择子和新栈的栈顶(最后一个被复制的参数)。这一切,对程序的编写者来说是透明的。所谓“透明”就是说,程序员不用关心栈的切换和参数的复制,他即使不知道还有栈切换这回事,也不会影响程序编写工作。因为,在栈切换前,pop edx,可以得到最后一个被压入的参数,在栈切换后,这条指令同样可以得到那个参数,尽管栈段和栈顶指针已经改变。
调用门描述符中的DPL和目标代码段描述符的DPL用于决定哪些特权级的程序可以访问此门。具体的规则是必须同时符合以下两个条件才行:
(一)当前特权级CPL和请求特权级RPL高于,或者和调用门描述符特权级DPL相同。即,在数值上
CPL≤调用门描述符的DPL
RPL≤调用门描述符的DPL
(二)当前特权级CPL低于,或者和目标代码段描述符特权级DPL相同。即,在数值上
CPL≥目标代码段描述符的DPL
举个例子,如果调用门描述符的DPL为2,那么,只有特权级0、1和2的程序才允许使用该调用门,特权级为3的程序使用此门将引发处理器异常中断。
如下图图14-011所示,调用门的DPL是特权级检查的下限。除此之外,目标代码段的特权级也是需要考虑的因素。调用门描述符中有目标代码段的选择子,它指向目标代码段的描述符。当一个程序通过调用门转移控制时,处理器还要检查目标代码段描述符的DPL,该DPL决定调用门特权级检查的上限。也就是说,只有那些特权级低于或者等于目标代码段DPL的程序才允许使用此门。
调用门描述符中有一些字段没有使用,固定为“0”。