当前任务
- 在一个多任务系统中,会有很多任务轮流执行,正在执行的叫当前任务(Current Task).
- 为了有效的在任务间实施隔离,处理器建议每个任务有自己的LDT (局部描述符表 Local Descriptor Table).描述符表保存了当前任务的段描述符.
- 在一个多任务系统中,当当前任务切换到另一个任务执行前要保存任务的当前状态也就是保护现场.这时,就需要TSS(任务状态段 Task State Segment).
LDT
在处理器中会有一个LDTR(局部描述符表寄存器)指向当前任务的LDT.LDTR包含了 32 位的线性基地址字段和 16 位的段界限字段.
我们知道在 32 位保护模式下段寄存器的段选择器保存的是段选择子,它的 位2 是TI(表指示器Table Indicator),
若
- TI=0,表示从GDT中加载描述符;
- TI=1.表示从LDT中加载描述符.
;如下例子
mov ax,0x000d
mov ds,ax
0x000d = 0000 0000 0000 1101,很容易知道TI=1,索引是"1",处理器执行以上指令时就会访问LDT.
TSS
TSS有固定的格式,最小尺寸是 104 字节.和LDT一样TSS用TR寄存器(Task Register),TR在寄存器中也只有一个.当任务切换发生的时候,TR寄存器指向新任务的TSS,这过程是这样的:
- 处理器将当前任务的现场信息保存到TR寄存器指向的TSS;
- 使TR指向新任务的TSS,并从新任务的TSS恢复现场.
TSS的格式如图:
图中灰色的是保留的部分.
全局空间和局部空间
- 操作系统需要把程序加载到内存并运行程序.
- 操作系统要提供大量的实例 , 数据或服务供程序调用,以简化程序编写.
- 同时在访问设备时消除潜在的竞争和冲突.
所以每个任务包含两个部分:全局部分和私有部分.
- 全局部分是所有任务共有的,含有操作系统的软件和库程序,以及可以调用的系统服务和数据;
- 私有部分包括每个任务各自的数据和代码.如图:
左是每个任务的全局空间和局部空间,右是多任务的全局空间和局部空间.
特权级保护
从上面已经看出处理器已经为特权级保护提供了可靠的硬件设施.当然,光有硬件是不够的,还需要一些规章制度,还要有人执行.为此处理器引入了特权级(Privilege Level).Intel处理器可以识别 4 个特权级别,分别是 0, 1, 2, 3.
特权级 0
特权级 0 是最高的特权级别,用于可靠性最高的程序.
通常,操作系统是为所有的程序服务的,它的可靠性最高,并且操作系统要负责对软硬件的控制,所以操作系统的主体必须拥有特权级 0.
特权级 1, 2
特权级 1, 2 是次于最高的特权级别(特权级 2 的特权级低于特权级 1),用于可靠性不如操作系统(或说是内核 Kernel)的系统服务程序.
比较典型的是设备驱动程序.
特权级 3
特权级 3是最低的特权级别,用于可靠性最低的程序.
应用程序的可靠性被视为最低的,通常不需要直接访问硬件和一些敏感的系统资源,调用设备驱动程序或操作系统实例能完成绝大多数工作.
实施特权保护
第一步(DPL)
为所有可管理的对象赋予一个特权级,以决定谁能访问.
每个描述符都有两个比特的DPL字段,可以取值 00, 01, 10 和 11,分别是特权级 0, 1, 2 和 3.
- 对于数据段DPL决定了访问它们的最低特权级别
例如:一个数据段的描述符其DPL = 1 ,则只有特权级为 0, 1 的程序可以访问它.特权为 2, 3 的程序试图读写它时,将会被处理器阻止,并引发异常中断.
第二步(CPL)
为当前程序赋予它拥有的特权级,以决定它能访问谁.
当处理器正在代码段取指令和执行指令时,那个代码段的特权级叫CPL(当前特权级 Current Privilege Level).正在被执行的代码段,其选择子位于段寄存器CS的段选择器,其低二位是CPL.
;如下例子
mov ax,0x000d
mov ds,ax
- 0x000d = 0000 0000 0000 1101,如果这个段选择子指向一个代码段,则可以知道它的 CPL=1 ,所以它能访问DPL= 1, 2, 3的数据段.
- 计算机系统是很脆弱的,甚至一条代码都能改变它整体的运行状态,比如停机指令 hlt ,这样的指令只能由最高的特权级别的程序执行.像这样只能由当 CPL=0 时才执行的指令,称作特权指令(Privileged Instuctions)
- 处理器还允许对个特权级别所能执行的 I/O 操作进行控制.如图:
图中展示的是EFLAGS(标志寄存器),其中的 位13, 位12 是IOPL位(即输入/输出特权级 I/O Privilege Level),它代表当前任务的I/O特权级. - 代码段的特权级检查是严格的,控制跳转只允许发生在两个特权相同的代码段之间.
当前特权级CPL = 0 的代码段,它只能转移到 DPL = 0的代码段,但连特权级比它低的代码段,即DPL = 1, 2, 3的代码段则不允许转移到. - 之前说了
操作系统要提供大量的实例 , 数据或服务供程序调用,以简化程序编写
所以,为了让特权级低的应用程序可以调用操作系统提供的实例,处理器提供了几种解决方法:
- 将特权级高的代码段定义为依从的:
代码段的段描述符的TYPE字段中有C位,代码段的段描述符的TYPE字段中有C位,
如果C=0,这代码段只能供特权级相同的代码段使用
如果C=1,这代码段叫依从的代码段,可以供特权级更低或相同的代码段使用
但是控制转移到依从的代码段,也是有条件滴.要求当前特权级低于或与目标代码段的描述符特权级相同,即是CPL >= 目标代码段的DPL
依从的代码段不是运行在它的DPL特权级上,而是运行在调用程序的特权级上,所以转移到依从的代码段,不改变CPL,也不改变CS中的CPL字段. - 依从的代码段似乎提供了十分多的便利,但他也有缺点:
如果,一个程序要访问操作系统的数据,或从磁盘读数据,读取键盘输入还是会受限制.
这时处理器就提供了调用门 (Call Gate),调用门也有它的描述符,它的描述符也是 64 位.在调用门中目标过程所在的代码段的选择子,以及段内偏移.要想通过调用门进行控制转移,可以用 jmp far 或 call far 指令,并把调用门描述符的选择子作为参数.
用 jmp far 可以通过调用门完成从特权级低的代码段转移特权级高的代码段,但不改变当前特权级(CPL不改变).
用 call far 可以通过调用门完成从特权级低的代码段转移特权级高的代码段,特权级会提升到目标代码段的特权级也就是处理器将在目标代码段的特权级执行(CPL=目标代码段的特权级). - 从高特权级的实例(过程)返回:
除了从高特权级的实例(过程)返回外,不允许从特权级高的代码段将控制转移特权级低的代码段,因为操作系统不会引用可靠性比自己低的代码.
第三步(RPL)
“RPL确保特权代码不会代替应用程序访问一个段,除非应用程序自己拥有访问这个段的权限”
"The RPL can be used to insure that privileged code does not access a segment on behalf of an application program unless the program itself has access privileges for that segment. " – 《Intel® 64 and IA-32 Architectures Software Developer’s Manual》5-8 Vol. 3A
由于I/O特权级限制,应用程序无法自己访问硬盘,所以要依靠操作系统提供的相应的实例
/*这里定义了一个函数,
*用于完成从磁盘中取出数据写入内存
*函数需要 3 个参数:
*int iba是逻辑扇区号
*int segmentSelector是被写入的数据段的段选择子
*int offset是写入的偏移地址
*int length是写入的长度单位为字节
*/
void writeToMemory(int iba,int segmentSelector,int offset,long length){
//此处省略
}
如果一个应用程序的编写者知道了操作系统的数据段选择子,而且希望访问甚至修改操作系统的数据段,虽然他不能在应用程序中访问,修改操作系统的数据段,因为他应用程序的CPL=3,处理器会把它拒之门外.但如果,他通过调用门,调用上面的函数,只要传入一个段选择子和一个偏移地址,就能修改操作系统的数据段.
所以处理器为了知道请求者的身份提供了RPL(请求特权级 Requested Privilege Level).不管实施控制转移,还是访问内存中的数据,必须先将段选择子加载到段寄存器DS, ES, CS, SS, GS 或 FS中.不管实施控制转移,还是访问数据段,都可以看成一个请求,请求者提供一个段选择子,请求访问指定段.
- 绝大多数请求者是当前程序,所以RPL=CPL.
- 也有请求者不是当前程序,例如在上面的例子中访问操作系统的数据段的请求者不是当前运行的这个函数,而是应用程序.
在这时单靠处理器无法防止例子中的非法访问,因为处理器只负责检查请求特权级RPL,如果操作系统没有鉴别请求者的身份,那么任何请求者都可以冒充操作系统提供最高请求特权级.
因为操作系统很清楚段选择子的来源(如果不清楚那么它就不能返回去继续执行),也就是真正的请求者,所以处理器交给它一个一定要完成的任务,就是操作系统要将参数中的段选择子的RPL设置为请求者的特权级.(可以用 arpl 指令) - 剩下的工作就是处理器了.每当处理器将段选择子送到段寄存器,比如:
mov ds,ax
处理器会检查以下两条件是否满足:
- 当前特权级CPL高于或者和数据段描述符的DPL相同,即在数值上 CPL <= 段描述符的DPL;
- 请求特权级CPL高于或者和数据段描述符的DPL相同,即在数值上 RPL <= 段描述符的DPL;
如果以上两条件不同时满足,处理器会阻止,并引发中断.
总结
- 控制直接转移到非依从代码段,要求当前特权级CPL和请求特权级RPL都和目标代码段的段描述符DPL相等,在数值上:
CPL=目标代码段的DPL
RPL=目标代码段的DPL - 控制直接转移到依从代码段,要求当前特权级CPL和请求特权级RPL都低于或等于目标代码段的段描述符DPL,在数值上:
CPL>=目标代码段的DPL
RPL>=目标代码段的DPL - 高特权级程序可以访问低特权的数据段,但低特权级程序不能访问高特权的数据段.访问数据段前要对段寄存器(DS, ES,GS,FS)进行修改,这时要求前特权级CPL和请求特权级RPL都高于或等于目标代码段的段描述符DPL,在数值上:
CPL<=目标数据段的DPL
RPL<=目标数据段的DPL - 对段寄存器SS进行修改时,要求当前特权级CPL和请求特权级RPL都和目标代码段的段描述符DPL相等,在数值上:
CPL=目标堆栈段的DPL
RPL=目标堆栈段的DPL