一致代码段 和 非一致代码段
系统要安全,必须保证内核与用户程序分离开,内核要安全,必须不能被用户来打扰。但是有的时候,用户程序也是需要访问内核中的部分数据,那怎么办?
于是操作系统就将内核中的段分为共享的代码段和非共享的代码段两部分。 其中一致代码段就是操作系统拿出来被共享的代码段,可以被低特权级的用户直接访问的代码。 一致代码段的限制作用: (1)特权级高的代码段不允许访问特权级低的代码段:即内核态不允许调用用户态下的代码。 (2)特权级低的代码段可以访问特权级高的代码段,但是当前的特权级不发生变化。即:用户态可以访问内核态的代码,但是用户态仍然是用户态。
非一致代码段:为了避免低特权级的访问而被操作系统保护起来的系统代码,也就是非共享代码。 非一致代码段的限制作用: (1)只允许同特权级间访问 (2)绝对禁止不同级间访问,即:用户态不能访问内核态,内核态也不访问用户态。
下图为一致码段与非一致码段的访问规则:
特权级转移
程序从一个代码段转移到另一个代码段之前,目标代码段的选择子会被加载到cs中。 作为加载过程中的一部分,处理器将会检查描述符的界限、类型、特权级等内容。 如果检验成功,cs将被加载,程序控制将转移到新的代码段中,从eip只是的位置开始执行!
程序控制转移的发生,可以是由指令jump、call、ret、sysenter、sysexit、int n 或者iret引起的,也可以由中断和异常机制引起。
使用jmp或call指令可以实现下列4种转移 (1) 目标操作数包含目标代码段的段选择子。 (2) 目标操作数指向一个包含目标代码段选择子的调用门描述符。 (3) 目标操作数指向一个包含目标代码段选择子的TSS。 (4) 目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS (任务状态段)。
这4种方式可以看做是两大类,一类是通过jmp和call的直接转移(上述第一种),另一类是通过某个描述符的间接转移(上述第2,3,4种)。
1、 通过jmp或call进行直接转移
从上面可以看出通过jmp和call所能进行的代码段间转移是非常有限的,对于非一致代码段,只能在相同特权级代码段之间转移。遇到一致代码段也最多能从低到高,而且CPL不会改变。如果想自由得进行不同特权级之间的转移,显然需要其它几种方式,即运用门描述符或者TSS.
任务状态段和控制门
每个任务有一个任务状态段 TSS,用于保存任务的有关信息,在任务内变换特权级和任务切换时,要用到这些信息。为了控制任务内发生特权级变换的转移,为了控制任务切换,一般要通过控制门进行这些转移。本文将介绍任务状态段和控制门。
系统段描述符
系统段是为了实现存储管理机制所使用的一种特别的段。在 80386 中,有两种系统段:任务状态段 TSS 和局部描述符表 LDT 段。
用于描述系统段的描述符称为系统段描述符。
1.系统段描述符的格式系统段描述符的一般格式如下表所示。
与存储段描述符相比,它们很相似,区分的标志是属性字节中的描述符类型位 DT 的值。DT=1 表示存储段,DT=0 表示系统段。系统段描述符中的段基地址和段界限字段与存储段描述符中的意义完全相同;属性中的 G 位、AVL 位、P 位和 DPL 字段的作用也完全相同。存储段描述符属性中的 D 位在系统段描述符中不使用,现用符号 X 表示。系统段描述符的类型字段 TYPE 仍是 4 位,其编码及表示的类型列于下表,其含义与存储段描述符的类型却完全不同。
从上表可见,只有类型编码为 2、1、3、9 和 B 的描述符才是真正的系统段描述符,它们用于描述系统段 LDT 和任务状态段 TSS,其它类型的描述符是门描述符。
利用前文定义的存储段描述符结构类型 DESC 仍能方便地在程序中说明系统段描述符。需要注意的是,系统段描述符的选择子不能用来读写系统段,要想读写系统段,必须使用别名技术。
任务状态段描述符
任务状态段 TSS 用于保存任务的各种状态信息。任务状态段描述符描述某个任务状态段 TSS 描述符分为 286TSS 和 386TSS 两类。TSS 描述符规定了任务状态段的基地址和任务状态段的大小等信息。例如,下面的描述符 TempTask 描述一个可用的 386 任务状态段,基地址是 123456H,以字节为单位的界限是 104,描述符特权级是 0。
TempTask DESC <104,3456H,12H,89H,,>
在装载任务状态段寄存器 TR 时,描述符中的段基地址和段界限等信息被装入到 TR 的高速缓冲寄存器中。在任务切换或执行 LTR指令时,要装载 TR 寄存器。
TSS 描述符中的类型规定:TSS 要么为“忙”,要么为“可用”。如果一个任务是当前正执行的任务,或者是用 TSS 中的链接字段沿挂起任务链接到当前任务上的任务,那么该任务是“忙”的任务;否则该任务为“可用”任务。
利用段间转移指令 JMP 和段间调用指令 CALL,直接通过 TSS 描述符或通过任务门可实现任务切换
( 在同一任务内,实现特权级从外层到内层变换的普通途径是使用段间调用指令 CALL,通过调用门进行转移;实现特权级从内层向外层变换的普通途径是使用段间返回指令 RET。注意,不能用 JMP 指令实现任务内不同特权级的变换。 )
调用门初体验
门也是描述符,结构图如下:
参照之前的代码段和数据段描述符:
可以看出前面的描述符有很大不同,主要定义了目标代码对应段的选择子、入口地址的偏移和一些属性等。
可以对比看看,byte5是一样的,都是表示属性!这个便于识别描述符类型。 这里S是0 (S 位用于指定描述符的类型(Descriptor Type)。当该位是“0”时,表示是一个系统段;为“1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。)
第四个字节里面有个paramcount ,这个会在下一章节详细解释,这个是跟TSS 任务状态段有关! 表示复制参数的个数(在堆栈的)!
可以看出一个门描述了一个选择子和一个偏移所制定的线性地址,程序正是通过这个地址进行转移的。门描述符分为4种:
- 调用门 call gates
- 中断门 Interrupt gates
- 陷阱门 Trap gates
- 任务门 Task gates
其中,中断门和陷阱门是特殊的调用门,书上后续会提到用法!
书上用一个例子介绍 调用门!
在pmtest3.asm 的基础增加代码段作为通过调用门转移的目标段!
代码pmtest4.asm ,代码有340行!仅仅列出增加的代码段:
....
LABEL_DESC_CODE_DEST: Descriptor 0,SegCodeDestLen-1, DA_C+DA_32; 非一致代码段,32
....
; 门 目标选择子,偏移,DCount, 属性
LABEL_CALL_GATE_TEST: Gate SelectorCodeDest, 0, 0, DA_386CGate+DA_DPL0
.....
SelectorCodeDest equ LABEL_DESC_CODE_DEST - LABEL_GDT
.....
SelectorCallGateTest equ LABEL_CALL_GATE_TEST - LABEL_GDT
.....
; 初始化测试调用门的代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE_DEST
mov word [LABEL_DESC_CODE_DEST + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE_DEST + 4], al
mov byte [LABEL_DESC_CODE_DEST + 7], ah
.......
; 测试调用门(无特权级变换),将打印字母 'C'
call SelectorCallGateTest:0
;call SelectorCodeDest:0
......
[SECTION .sdest]; 调用门目标段
[BITS 32]
LABEL_SEG_CODE_DEST:
;jmp $
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 12 + 0) * 2 ; 屏幕第 12 行, 第 0 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'C'
mov [gs:edi], ax
retf
SegCodeDestLen equ $ - LABEL_SEG_CODE_DEST
; END of [SECTION .sdest]
........
从代码可以看出,目标代码段如下:
[SECTION .sdest]; 调用门目标段
[BITS 32]
LABEL_SEG_CODE_DEST:
;jmp $
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 12 + 0) * 2 ; 屏幕第 12 行, 第 0 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'C'
mov [gs:edi], ax
retf
SegCodeDestLen equ $ - LABEL_SEG_CODE_DEST
; END of [SECTION .sdest]
可以看出该目标段就是显示红色的c字!
使用call retf 成对调用指令!
还有添加了代码段的描述符!就跳过说明了!
添加的调用们描述符:
; 门 目标选择子,偏移,DCount, 属性
LABEL_CALL_GATE_TEST: Gate SelectorCodeDest, 0, 0, DA_386CGate+DA_DPL0
可以看出使用Gate而不是Descriptor!在pm.inc的定义如下:
;
; 门
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0FFFFh) ; 偏移1
dw %1 ; 选择子
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性
dw ((%2 >> 16) & 0FFFFh) ; 偏移2
%endmacro ; 共 8 字节
;
属性里面是DA_386CGate ,在pm.inc的定义如下:
DA_386CGate EQU 8Ch ; 386 调用门类型值
可以看出paramCount是0,且P = 1; DPL = 0 ; S = 0; TYPE = 0X0C,在系统段或者门描述符定义的是386调用门!
而且里面指定的段选择子是SelectorCodeDest!偏移地址是0,表示要跳转到目标代码段的开头处!
调用门对应的选择子的定义如下:
SelectorCallGateTest equ LABEL_CALL_GATE_TEST - LABEL_GDT
所以调用门准备就绪,指向的位置是SelectorCodeDest:0, 也就是标号LABEL_SEG_CODE_DEST处的代码!
代码是用call 方法调用的
; 测试调用门(无特权级变换),将打印字母 'C'
call SelectorCallGateTest:0
;call SelectorCodeDest:0 ; 也可以修改成直接调用目标代码段的地址! 运行效果也一样!
调用门主要是用来实现不同特权级的代码之间的转移。
下面介绍一个使用调用门进行转移时特权级检验的规则。
假设我们想由代码A 转移到代码B ,运用一个调用门G ,即调用门G中的目标选择子指向代码B的段。
实际上,涉及了几个要素:CPL、RPL、代码B的DPL(记作DPL_B)、调用门G的DPL(记作DPL_G)!
可以从前面描述得知
A 访问G 这个调用门,规则就是相当于访问一个数据段,也就是要求CPL RPL都小于或者等于DPL_G!
除了这一步符合要求之外,系统还将比较CPL和DPL_B , 如果是一致代码段的话,要求DPL_B<= CPL !
如果是非一致代码段的话,call 指令和jmp指令又有所不同!!!
在用call指令时,要求DPL_B <= CPL ; 在用jmp指令时,只能是DPL_B = CPL !
调用门使用时特权检查的规则如表所示!
call | jmp | |
目标是一致代码段 | CPL <= DPL_G, RPL <= DPL_G , DPL_B <= CPL | |
目标是非一致代码段 | CPL <= DPL_G, RPL <= DPL_G , DPL_B <= CPL | CPL <= DPL_G, RPL <= DPL_G , DPL_B = CPL | </table> 也就是说通过调用门和call指令, 可以实现从低特权级到高特权级的转移,无论目标代码段是一致的还是非一致的。 特权级的检验不仅仅是在特权级的变化,堆栈也要发生变化。处理器的这种机制避免了高特权级的过程由于栈空间不足而崩溃! 而且如果不同特权级共享一个堆栈,会导致高特权级的程序受到干扰!