CPU不仅仅在程序访问数据段和堆栈段的时候进行权限级别检查,当程序控制权转换的时候也会进行权限级别检查。程序控制权转换的情况很多,各种情况下检查的方式以及涉及到的检查项都是不同的。这篇文章主要描述了各种代码控制权转换过程中涉及到的各种检查并且配以相应的示例,示例代码是根据《Task》中的代码修改的,托管在https://github.com/activesys/learning_cpu/tree/master/x86/protection_5
程序控制权转换
很多指令都可以引起代码控制权的转换,例如call, jmp, int, lcall, ljmp, sysenter, sysexit以及syscall, sysret等等,但是不同的指令会引起不同类型的控制权转换,即使相同的指令接不同类型的选择子也会引起不同类型的控制权转换,总结起来有下面几种:
- 短跳转:这种跳转是在段内的控制权转换,不进行权限级别检查。
- 长跳转:这种跳转是段间的控制权转换,进行权限级别检查,这篇文章主要关注的就是这类控制权转换。
- 中断和异常:中断和异常引起的控制权转换在学习中断和异常的时候再描述。
- 任务切换:任务切换引起的控制权转换已经在《Task》中描述了。
- sysenter,sysexit以及syscall,sysret指令实现的快速系统调用引起的控制权转换。
程序控制权转换时的权限检查
在lcall或者ljmp指令后面接代码段选择子来实现段间的程序控制权转换,这个时候CPU要实施权限级别检查,检查涉及到CPL, RPL, DPL以及代码段描述符中的C位。C标志位的不同导致了代码段分为nonconforming和conforming,针对这两种类型的代码段的权限级别检查也是不同的。
nonconforming
在跳转到nonconforming代码段的时候,CPU要求CPL == DPL && CPL >= RPL。当段选择子成功的加载到%cs之后,CPL并不改变。这样看来要访问nonconforming代码段必须是同级别的代码,即使是高权限级别代码访问低权限级别代码也是不行的。
为了验证对nonconforming代码段访问过程中的权限级别检查,我们必须添加两个nonconforming代码段,一个DPL==0,一个DPL==3,同时还有这两个段的“配套设置”:数据段,堆栈段和作为屏幕输出的扩展段:
# test code data
# attr = 0x4092(G=0,D/B=1,L=0,AVL=0,P=1,DPL=00,S=1,TYPE=0010)
.equ TEST_CODE_DPL0_DATA_BASE, 0x0000
.equ TEST_CODE_DPL0_DATA_LIMIT, 0xffff
.equ TEST_CODE_DPL0_DATA_ATTR, 0x4092
.equ TEST_CODE_DPL0_DATA_SELECTOR, 0x58 # test code stack
# attr = 0x4092(G=0,D/B=1,L=0,AVL=0,P=1,DPL=00,S=1,TYPE=0010)
.equ TEST_CODE_DPL0_STACK_BASE, 0xc200
.equ TEST_CODE_DPL0_STACK_LIMIT, 0xffff
.equ TEST_CODE_DPL0_STACK_ATTR, 0x4092
.equ TEST_CODE_DPL0_STACK_SELECTOR, 0x60
.equ TEST_CODE_DPL0_STACK_INIT_ESP, 0xd000 # test code video
# attr = 0x4092(G=0,D/B=1,L=0,AVL=0,P=1,DPL=00,S=1,TYPE=0010)
.equ TEST_CODE_DPL0_VIDEO_BASE, 0x0b8000
.equ TEST_CODE_DPL0_VIDEO_LIMIT, 0xffff
.equ TEST_CODE_DPL0_VIDEO_ATTR, 0x4092
.equ TEST_CODE_DPL0_VIDEO_SELECTOR, 0x68 # test code data
# attr = 0x40f2(G=0,D/B=1,L=0,AVL=0,P=1,DPL=11,S=1,TYPE=0010)
.equ TEST_CODE_DPL3_DATA_BASE, 0x0000
.equ TEST_CODE_DPL3_DATA_LIMIT, 0xffff
.equ TEST_CODE_DPL3_DATA_ATTR, 0x40f2
.equ TEST_CODE_DPL3_DATA_SELECTOR, 0x73 # test code stack
# attr = 0x40f2(G=0,D/B=1,L=0,AVL=0,P=1,DPL=11,S=1,TYPE=0010)
.equ TEST_CODE_DPL3_STACK_BASE, 0xc200
.equ TEST_CODE_DPL3_STACK_LIMIT, 0xffff
.equ TEST_CODE_DPL3_STACK_ATTR, 0x40f2
.equ TEST_CODE_DPL3_STACK_SELECTOR, 0x7b
.equ TEST_CODE_DPL3_STACK_INIT_ESP, 0xd000 # test code video
# attr = 0x40f2(G=0,D/B=1,L=0,AVL=0,P=1,DPL=11,S=1,TYPE=0010)
.equ TEST_CODE_DPL3_VIDEO_BASE, 0x0b8000
.equ TEST_CODE_DPL3_VIDEO_LIMIT, 0xffff
.equ TEST_CODE_DPL3_VIDEO_ATTR, 0x40f2
.equ TEST_CODE_DPL3_VIDEO_SELECTOR, 0x83 # nonconforming code DPL==0
# attr = 0x4098(G=0,D/B=1,L=0,AVL=0,P=1,DPL=00,S=1,TYPE=1000)
.equ NONCONFORMING_CODE_DPL0_BASE, 0xc000
.equ NONCONFORMING_CODE_DPL0_LIMIT, 0xffff
.equ NONCONFORMING_CODE_DPL0_ATTR, 0x4098
.equ NONCONFORMING_CODE_DPL0_SELECTOR, 0x88 # nonconforming code DPL==3
# attr = 0x40f8(G=0,D/B=1,L=0,AVL=0,P=1,DPL=11,S=1,TYPE=1000)
.equ NONCONFORMING_CODE_DPL3_BASE, 0xc040
.equ NONCONFORMING_CODE_DPL3_LIMIT, 0xffff
.equ NONCONFORMING_CODE_DPL3_ATTR, 0x40f8
.equ NONCONFORMING_CODE_DPL3_SELECTOR, 0x93
实现两个nonconforming的代码都放在code.s中:
###############################################################
# nonconforming code DPL == 0
_nonconforming_code_dpl0:
xorl %eax, %eax
movl $TEST_CODE_DPL0_DATA_SELECTOR, %eax
movw %ax, %ds
movl $TEST_CODE_DPL0_STACK_SELECTOR, %eax
movw %ax, %ss
movl $TEST_CODE_DPL0_STACK_INIT_ESP, %esp
movl $TEST_CODE_DPL0_VIDEO_SELECTOR, %eax
movw %ax, %es
movl $NULL_SELECTOR, %eax
movw %ax, %fs
movw %ax, %gs movl $_nonconforming_code_dpl0_msg, %esi
movl $_nonconforming_code_dpl0_msg_len, %ecx
movl $TEST_CODE_VIDEO_OFFSET, %edx
call _code_echo jmp . .space 0x40-(.-_start), 0x00 ###############################################################
# nonconforming code DPL == 3
_nonconforming_code_dpl3:
xorl %eax, %eax
movl $TEST_CODE_DPL3_DATA_SELECTOR, %eax
movw %ax, %ds
movl $TEST_CODE_DPL3_STACK_SELECTOR, %eax
movw %ax, %ss
movl $TEST_CODE_DPL3_STACK_INIT_ESP, %esp
movl $TEST_CODE_DPL3_VIDEO_SELECTOR, %eax
movw %ax, %es
movl $NULL_SELECTOR, %eax
movw %ax, %fs
movw %ax, %gs movl $_nonconforming_code_dpl3_msg, %esi
movl $_nonconforming_code_dpl3_msg_len, %ecx
movl $TEST_CODE_VIDEO_OFFSET, %edx
call _code_echo jmp . .space 0x80-(.-_start), 0x00
code.s中还添加了用于两个nonconforming代码段输出的消息数据:
###############################################################
# message data
_nonconforming_code_dpl0_msg:
.ascii "In nonconforming code segment, DPL == 0."
_nonconforming_code_dpl0_msg_end:
.equ _nonconforming_code_dpl0_msg_len, _nonconforming_code_dpl0_msg_end - _nonconforming_code_dpl0_msg
_nonconforming_code_dpl3_msg:
.ascii "In nonconforming code segment, DPL == 3."
_nonconforming_code_dpl3_msg_end:
.equ _nonconforming_code_dpl3_msg_len, _nonconforming_code_dpl3_msg_end - _nonconforming_code_dpl3_msg
通过权限级别检查
万事俱备了,可以跳转到nonconforming代码段了,首先在CPL==0时跳转到DPL==0的nonconforming代码段,在kernel.s中加入长跳转代码:
lcall $NONCONFORMING_CODE_DPL0_SELECTOR, $0x00
运行结果:
从运行的结果可以看出从kernel代码跳转到了DPL==0的nonconforming代码段。
接下来试验一下CPL==3的时候跳转到DPL==3的nonconforming代码段,在user.s中加入长跳转:
lcall $NONCONFORMING_CODE_DPL3_SELECTOR, $0x00
运行结果:
从结果上看控制权是从kernel代码转移到user代码,这时候CPL==3,然后通过lcall转移到了nonconforming代码段。
没有通过权限级别检查
上面的例子都是通过的权限级别检查的,再来看看不能通过检查的情况,也就是当CPL!=DPL的时候,首先在CPL==0的代码中访问DPL==3的nonconforming代码段,在kernel.s加入lcall长跳转:
lcall $NONCONFORMING_CODE_DPL3_SELECTOR, $0x00
运行结果:
结果是在kernel中触发了#GP。
再来看看在CPL==3的代码中访问DPL==0的nonconforming代码段,在user.s中调用lcall长跳转:
lcall $NONCONFORMING_CODE_DPL0_SELECTOR, $0x00
运行结果:
从运行结果中可以看出在CPL==3的时候访问DPL==0的nonconforming代码段触发了#GP。
conforming
控制权切换到conforming代码段的时候进行的权限级别检查与nonconforming是不同的,conforming代码段描述中的DPL表示的是能够访问该代码段的最高权限级别,例如DPL==0,那么CPL==0~3都可以访问,但是如果DPL==3,那么只有CPL==3的代码段才可以访问。控制权转移到conforming代码段之后CPL并不改变,例如从CPL==3的代码段转换到DPL==0的conforming代码段,转换之后CPL仍然是3。
为了验证切换到conforming代码段时进行的权限级别检查,我们添加了三个代码段描述符:
# conforming code DPL==0
# attr = 0x409c(G=0,D/B=1,L=0,AVL=0,P=1,DPL=00,S=1,TYPE=1100)
.equ CONFORMING_CODE_DPL0_BASE, 0xc080
.equ CONFORMING_CODE_DPL0_LIMIT, 0xffff
.equ CONFORMING_CODE_DPL0_ATTR, 0x409c
.equ CONFORMING_CODE_DPL0_SELECTOR, 0x98 # conforming code DPL==3
# attr = 0x40fc(G=0,D/B=1,L=0,AVL=0,P=1,DPL=11,S=1,TYPE=1100)
.equ CONFORMING_CODE_DPL3_BASE, 0xc0c0
.equ CONFORMING_CODE_DPL3_LIMIT, 0xffff
.equ CONFORMING_CODE_DPL3_ATTR, 0x40fc
.equ CONFORMING_CODE_DPL3_SELECTOR, 0xa3 # conforming code DPL==0, CPL==3
# attr = 0x409c(G=0,D/B=1,L=0,AVL=0,P=1,DPL=00,S=1,TYPE=1100)
.equ CONFORMING_CODE_DPL0_CPL3_BASE, 0xc100
.equ CONFORMING_CODE_DPL0_CPL3_LIMIT, 0xffff
.equ CONFORMING_CODE_DPL0_CPL3_ATTR, 0x409c
.equ CONFORMING_CODE_DPL0_CPL3_SELECTOR, 0xa8
一个DPL==0,一个DPL==3,还有一个是为了在CPL==3的时候访问的DPL==0的代码段。相应的在code.s中有三段conforming代码:
###############################################################
# conforming code DPL == 0
_conforming_code_dpl0:
xorl %eax, %eax
movl $TEST_CODE_DPL0_DATA_SELECTOR, %eax
movw %ax, %ds
movl $TEST_CODE_DPL0_STACK_SELECTOR, %eax
movw %ax, %ss
movl $TEST_CODE_DPL0_STACK_INIT_ESP, %esp
movl $TEST_CODE_DPL0_VIDEO_SELECTOR, %eax
movw %ax, %es
movl $NULL_SELECTOR, %eax
movw %ax, %fs
movw %ax, %gs movl $_conforming_code_dpl0_msg, %esi
movl $_conforming_code_dpl0_msg_len, %ecx
movl $TEST_CODE_VIDEO_OFFSET, %edx
call _code_echo jmp . .space 0xc0-(.-_start), 0x00 ###############################################################
# conforming code DPL == 3
_conforming_code_dpl3:
xorl %eax, %eax
movl $TEST_CODE_DPL3_DATA_SELECTOR, %eax
movw %ax, %ds
movl $TEST_CODE_DPL3_STACK_SELECTOR, %eax
movw %ax, %ss
movl $TEST_CODE_DPL3_STACK_INIT_ESP, %esp
movl $TEST_CODE_DPL3_VIDEO_SELECTOR, %eax
movw %ax, %es
movl $NULL_SELECTOR, %eax
movw %ax, %fs
movw %ax, %gs movl $_conforming_code_dpl3_msg, %esi
movl $_conforming_code_dpl3_msg_len, %ecx
movl $TEST_CODE_VIDEO_OFFSET, %edx
call _code_echo jmp . .space 0x0100-(.-_start), 0x00 ###############################################################
# conforming code DPL == 0, CPL == 3
_conforming_code_dpl0_cpl3:
xorl %eax, %eax
movl $TEST_CODE_DPL3_DATA_SELECTOR, %eax
movw %ax, %ds
movl $TEST_CODE_DPL3_STACK_SELECTOR, %eax
movw %ax, %ss
movl $TEST_CODE_DPL3_STACK_INIT_ESP, %esp
movl $TEST_CODE_DPL3_VIDEO_SELECTOR, %eax
movw %ax, %es
movl $NULL_SELECTOR, %eax
movw %ax, %fs
movw %ax, %gs movl $_conforming_code_dpl0_cpl3_msg, %esi
movl $_conforming_code_dpl0_cpl3_msg_len, %ecx
movl $TEST_CODE_VIDEO_OFFSET, %edx
call _code_echo jmp .
通过权限级别检查
首先来看看在CPL==0级别的代码切换到DPL==0的conforming代码段的情况,在kernel.s中加入如下代码:
lcall $CONFORMING_CODE_DPL0_SELECTOR, $0x00
运行结果:
从结果中可以看出成功的切换到了DPL==0的conforming代码段。
再来看看从CPL==3的代码段切换至DPL==3的conforming代码段,在user.s中加入如下代码:
lcall $CONFORMING_CODE_DPL3_SELECTOR, $0x00
运行结果:
从结果中可以看出从CPL==3的代码段成功的切换到了DPL==3的conforming代码段。
没有通过权限级别检查
conforming代码段描述符中的DPL表示的是能够访问该代码段的CPL的最高权限,那么在CPL==0的代码段中访问DPL==3的conforming代码段必然会触发异常,为了做这个验证,在kernel.s加入如下代码:
lcall $CONFORMING_CODE_DPL3_SELECTOR, $0x00
运行结果如你我所愿:
我们再来试验一下在CPL==3的情况下访问DPL==0的conforming代码段,在user.s中加入下面代码:
lcall $CONFORMING_CODE_DPL0_SELECTOR, $0x00
运行结果:
怎么会触发异常了呢?按照conforming代码段的权限级别检查规则,这个测试应该是成功的,具体原因在哪里呢?其实这个#GP不是代码控制权转换过程中的权限检查产生的,而是进入conforming代码段之后加载段寄存器时的权限检查产生的,因为conforming代码段的控制权转换过程中,CPL不变,所以进入conforming代码段之后CPL仍然是3,但是要加载的代码段,堆栈段等段都是DPL==0的,这样就触发了#GP。
还记得最开始的时候我们准备了一段DPL==0,CPL==3的conforming代码段吗,现在可以派上用场了,它与DPL==0的conforming代码段的区别就是内部加载的段寄存器都是DPL==3的段,这样就不会触发#GP了。为了验证我们的猜测,在user.s中加入如下代码:
lcall $CONFORMING_CODE_DPL0_CPL3_SELECTOR, $0x00
运行结果如你我所愿:
call gate
到目前为止似乎不能够在不同权限级别之间进行切换,因为无论是nonconforming代码段还是conforming代码段,在发生控制权转换的过程中CPL都是不变的。为了实现权限级别切换,CPU提供了Call-Gate描述符。但是权限切换是有严格要求的,不是所有的情况都能够实现权限切换。
先来看看call-gate机制:
这是Intel官方文档中关于call-gate机制的描述,长跳转指令通过门选择子以及偏移量来选择门描述符,这里的偏移量CPU不会使用,但是必须提供,所以可以是任何值。门描述符中有段选择子以及偏移量,通过段选择子获得段描述符其中的段基址,然后与门描述符中的偏移量一同计算出实际的代码段。
这样通过call-gate来进行控制权转换的过程中进行权限级别检查时涉及的标志位就是:
- CPL
- call-gate的RPL
- 门描述符的DPL
- 目标代码段描述符的DPL
- 目标代码段描述符的C标志位
通过call-gate访问代码段的时候lcall和ljmp导致的权限检查是不同的:
这是Intel官方文档中给出的通过call-gate访问代码段的时候的权限检查规则,当访问nonconforming代码段的时候lcall和ljmp的检查规则是不一致的。
从表中可以看出只有lcall命令可以从低权限级别访问高权限级别的nonconforming代码段,对于conforming代码段,lcall和ljmp都可以实现从低权限级别到高权限级别的访问。但是只有lcall从低权限级访问高权限级别的nonconforming代码段的时候才会发生CPL改变,CPL变成nonconforming代码段的DPL,访问conforming代码段CPL仍然是不变的。
原来在user.s中访问DPL==0的nonconforming代码段是会触发#GP的,现在可以通过call-gate实现这样的访问。为了实现call-gate机制要在GDT中添加一个call-gate描述符:
# nonconforming code call gate
# attr = 0x00ec(G=0,D/B=0,L=0,AVL=0,P=1,DPL=11,S=0,TYPE=1100)
.equ NONCONFORMING_CALL_GATE_BASE, 0x88
.equ NONCONFORMING_CALL_GATE_LIMIT, 0x00
.equ NONCONFORMING_CALL_GATE_ATTR, 0x00ec
.equ NONCONFORMING_CALL_GATE_SELECTOR, 0xb0
它指向了DPL==0的nonconforming代码段。在user.s中call-gate选择子来访问nonconforming代码段:
lcall $NONCONFORMING_CALL_GATE_SELECTOR, $0x00
运行结果:
从运行结果可以看出,代码实现了从CPL==3的代码段转换到了DPL==0的nonconforming代码段,如果不使用call-gate机制是会触发#GP的,这说明了call-gate的作用。