特权级
为了提供保护,CPU识别4个特权级,0~3级,数字越小表示的特权级越高,数字越大表示的特权级越小。
这是Intel官方文档中关于特权级的描述,所以这些特权级又叫ring0~ring3。从图中可以看出ring0运行的是比较核心的代码,例如kernel,越到外层运行的代码权限越低,运行的代码也不那么核心了。4个特权级不一定全都被使用,在只使用两个特权级的代码中使用的是ring0和ring3。CPU提供这样的特权级保护是为了防止权限级别较低的代码访问权限级别较高的代码的资源,如果随意访问会引起安全问题。CPU通过权限级别检查来保证正确的权限级别间的访问,如果发现违规就会触发#GP。
为了实施权限级别检查CPU会检查如下三种权限级别之间的关系:
- CPL:CPL是当前代码段寄存器和堆栈段寄存器的第0位和第1位的值,CPL也决定了当前代码处哪个ring中。当发生任务切换或者是代码段转换的时候CPL会随之改变。
- DPL:DPL是段的权限级别,它由段描述符或者门描述符的DPL位决定。对于不同的段DPL的含义是不同的:
- 数据段:对于数据段DPL表示能够访问该数据段的代码或者任务的最低权限级别,例如数据段的DPL==1,那么只有CPL==0或者CPL==1的代码或者任务可以访问该段。
- 非一致性代码段:DPL表示能够访问该代码段的权限级别,例如DPL == 1,那么只有CPL==1的代码段能够访问该段,即使CPL==0也不行。
- 调用门:与数据段的DPL一致。
- 通过调用门访问的代码段:DPL表示能够访问该段的最高权限级别,例如DPL==2那么只有CPL==2或者CPL==3的任务和程序能够访问该段。这与数据段的DPL的含义正好相反,这也是低权限级别代码访问高权限级别代码的方式。
- TSS:TSS的DPL与数据段一致。
- RPL:RPL叫做要求权限级别,位于段选择子第0位和第1位,它可以覆盖当前任务或者程序的CPL,当加载一个段选择子的时候,CPU会将CPL和RPL中权限较低的作为访问段的权限级别。例如当CPL==0,RPL==3的时候要访问一个DPL==2的数据段,那么实际访问数据段的时候的权限级别是3,因而会触发#GP。再有当CPL==3,RPL==0的时候访问DPL==2的数据段,那么实际访问的权限级别仍然是3,也会触发#GP。
访问数据段时候的特权级检查
CPU会根据要访问的段的类型进行不同的权限级别的检查,访问数据段时的检查主要是检查访问的权限和DPL的关系,Intel的官方文档中给出了一个访问的图例,非常详细的列举了各种情况。
我们的例子代码没有实现那么多种详细的访问情况,但是也足以验证在访问数据段时的权限级别检查了,下面这张图就是我们的例子中的数据段的访问情况:
这里实现了三种正常的访问情况:
- CPL==0, RPL==0访问DPL==0的数据段。
- CPL==3, RPL==3访问DPL==3的数据段。
- CPL==0, RPL==3访问DPL==3的数据段。
- CPL==0, RPL==3访问DPL==0的数据段。
- CPL==3, RPL==0访问DPL==0的数据段。
示例
# test data CPL == 0
# attr = 0x4092(G=0,D/B=1,L=0,AVL=0,P=1,DPL=00,S=1,TYPE=0010)
.equ TEST_DATA_DPL0_BASE, 0xc000
.equ TEST_DATA_DPL0_LIMIT, 0xffff
.equ TEST_DATA_DPL0_ATTR, 0x4092
.equ TEST_DATA_DPL0_SELECTOR_RPL0, 0x58
.equ TEST_DATA_DPL0_SELECTOR_RPL3, 0x5b
# test data CPL == 3
# attr = 0x4092(G=0,D/B=1,L=0,AVL=0,P=1,DPL=11,S=1,TYPE=0010)
.equ TEST_DATA_DPL3_BASE, 0xc100
.equ TEST_DATA_DPL3_LIMIT, 0xffff
.equ TEST_DATA_DPL3_ATTR, 0x40f2
.equ TEST_DATA_DPL3_SELECTOR_RPL0, 0x60
.equ TEST_DATA_DPL3_SELECTOR_RPL3, 0x63
添加了一个数据段段文件data.s:
#
# test data
#
.code32
.globl _start
_start:
###############################################################
# CPL0 message
_test_data_cpl0_msg:
.ascii "TEST DATA MESSAGE, CPL == 0, DPL == 0."
.ascii "TEST DATA MESSAGE, CPL == 3, DPL == 0."
dummy1:
.space 0x100-(.-_start), 0x00
###############################################################
# CPL3 message
_test_data_cpl3_msg:
.ascii "TEST DATA MESSAGE, CPL == 3, DPL == 3."
.ascii "TEST DATA MESSAGE, CPL == 0, DPL == 3."
dummy2:
.space 0x200-(.-_start), 0x00
数据文件中为两个数据段分别定义了两条消息,在后面我们会看到通过将这些消息打印到屏幕上来验证代码段对于数据段的访问。
权限级别检查成功
第一种成功的访问数据段的形式情况就是当代码运行到kernel,这时候CPL==0,通过RPL==0的选择子来访问DPL==0的数据段,并将数据段内容打印到屏幕上: # load test data DPL == 0
movl $TEST_DATA_DPL0_SELECTOR_RPL0, %eax
movw %ax, %ds
movl $TEST_DATA_CPL0_DPL0_MSG_OFFSET, %esi
movl $TEST_DATA_CPL0_DPL0_MSG_LENGTH, %ecx
movl $CPL0_DPL0_VIDEO_OFFSET, %edx
call _kernel_echo
第二种成功访问数据段的情况与第一种情况相同,就是当代码运行到user的时候,这个时候CPL==3,通过RPL==3的选择子来访问DPL==3的数据段,并且将数据段的内容打印到屏幕上:
# load test data DPL == 3
movl $TEST_DATA_DPL3_SELECTOR_RPL3, %eax
movw %ax, %ds
movl $TEST_DATA_CPL3_DPL3_MSG_OFFSET, %esi
movl $TEST_DATA_CPL3_DPL3_MSG_LENGTH, %ecx
movl $CPL3_DPL3_VIDEO_OFFSET, %edx
call _user_echo
第三种成功的情况是当代码从user回到kernel中,这时候CPL==0,通过RPL==3的选择子来访问DPL==3的数据段,并且将段的内容打印到屏幕上:
# load test data DPL == 3
movl $TEST_DATA_DPL3_SELECTOR_RPL3, %eax
movw %ax, %ds
movl $TEST_DATA_CPL0_DPL3_MSG_OFFSET, %esi
movl $TEST_DATA_CPL0_DPL3_MSG_LENGTH, %ecx
movl $CPL0_DPL3_VIDEO_OFFSET, %edx
call _kernel_echo
代码运行的结果:
通过打印的结果可以看出代码段正确的访问了数据段并且都通过了权限级别检查。
权限级别检查失败
第一种权限级别检查失败是在user代码中试图访问DPL==0的数据段: movl $TEST_DATA_DPL0_SELECTOR_RPL0, %eax
movw %ax, %ds
运行结果:
movl $TEST_DATA_DPL0_SELECTOR_RPL3, %eax
movw %ax, %ds
运行结果:
加载SS寄存器时的特权级检查
加载SS寄存器的时候进行的权限级别检查也涉及到了CPL, RPL和DPL,但是检查规则与数据段不同,CPU要求加载SS寄存器时CPL==RPL==DPL,否则就会触发#GP。在上面的代码例子中验证这个检查,在kernel代码中通过RPL==3的堆栈段选择子来访问DPL==0的堆栈段:
movl $KERNEL_STACK_SELECTOR_RPL3, %eax
movw %ax, %ss
运行结果:
运行结果看上去和上面的是一样的,但是触发的机制是不同的。