引言
- 本章节需要自己通过实际实验来摸索 DPL、RPL、CPL 三者之间的关系,实验方式就是尝试改变不同的 DPL、RPL 的值,然后观察程序的运行状态及结果
- 实验过程在这里就不一一记录了,建议自己摸索,这样才能较为深入的体会特权级相关知识
特权级转移小建议
- 建议绝大多数情况下同一个类段的相关特权级保持一致,即如果代码段 DPL = 0,那么与之对应的栈段、数据段等的 DPL 也要设置为 0,又或者不要出现对应的 RPL != DPL 的情况
- 我们在上一章节实现的 loader.asm 这个代码基础上做个小实验看一下
- 仅将栈段描述符 STACK32_DESC 的特权级改为 3
; STACK32_DESC : Descriptor 0, TOP_OF_STACK32, DA_DRW + DA_32
STACK32_DESC : Descriptor 0, TOP_OF_STACK32, DA_DRW + DA_32 + DA_DPL3
- 运行一下,程序并不能正确执行
- 不是满足 CPL <= DPL 并且 RPL <= DPL 这个条件吗?怎么程序还是不能执行成功呢?
- 原因就在于特权级 0 的代码段使用了特权级 3 的栈,CPU 肯定不会同意你这么干的
- 所以绝大多数情况下相关特权级定义一致能够有效的避免很多错误
段的分类
- 从 保护模式 中我们知道,描述符分系统描述符和非系统描述符
- 系统段和非系统段的区分方式就是段描述符中的 S 位,系统描述符(S=0),非系统描述符(S=1)
- 系统描述符包含 LDT,TSS 和各种门等
- 非系统描述符其实说的就是代码段或数据段描述符
- 对于代码段,又可以分为两类,一致性代码段和非一致性代码段
- 那么怎么区分一致性代码段和非一致性代码段呢?
- 见下图,段描述符中 TYPE 字段中的 C 位为 1,则为一致性代码段, C 位为 0,则为非一致性代码段
- 说了等于没说,那么如何直观理解一致性代码段和非一致性代码段的区别的呢?
- 在不借助调用门的情况下:
- 非一致性代码段:代码段之间只能平级转移(CPL == DPL,RPL <= DPL)
- 一致性代码段:支持低特权级代码段向高特权级代码段转移(CPL >= DPL),虽然可以成功转移到高特权级的代码段,但是当前特权级 CPL 不变
- 注意:数据段只有一种,没有一致性和非一致性的区分,并且数据段不允许被低特权级的代码段访问
验证一致性代码段特性
- 目的:验证一致性代码段是否支持低特权级代码段向高特权级代码段转移,当前特权级 CPL 是否发生改变
- 首先我们实验所用的基础代码依旧是上面实验所用的基础代码:loader.asm
- 原程序中 Task A(特权级 3)通过调用门跳转到特权级 0 执行打印,现在我们先把打印程序代码段属性改为一致性代码段即可
; FUNC_DESC : Descriptor 0, FUNC_SEG_LEN - 1, DA_C + DA_32 FUNC_DESC : Descriptor 0, FUNC_SEG_LEN - 1, DA_CCO + DA_32
- Task A 中原调用门方式要改为 jmp 直接跳转
; call FUNC_PRINT_SELECTOR : 0 jmp FUNC_SELECTOR : printOffset
- 实验结果,程序成功运行,现象与所期待的一致
- 用反汇编方式断点调试,发现跳转后特权级并未改变,验证我们上面所说的理论
深入理解调用门
- 调用门用于低特权级代码段向高特权级代码段转移
- 调用门描述符的特权级低于或小于当前特权级(DPL(Gate) >= CPL)
- 用图形的形式直观理解
- 关于调用门的注意事项
- 调用门支持特权级同级转移
- 调用门同级转移被处理位普通函数调用或直接调用
- call 通过调用门提升特权级,jmp 只能同级转移
- 通过调用门降特权级返回(retf)时,CPU 会对目标代码段及栈段进行特权级检查;对相关段寄存器强制清零
- 通过反汇编后断点调试,发现执行 retf 指令(特权级降低)后,相关段寄存器都被清零了,这是为什么呢?
- 原因就是处理器为了内核数据的安全,当发生高特权级代码段向低特权级代码段转移时,CPU 会将这些寄存器全部自动清零,这样子低特权级程序就没有机会非法的访问内核数据了
IO 特权级
- 在保护模式下,特权级检查保护机制不光体现在数据和代码的访问,还体现在 I/O 读写控制上
- I/O 读写特权是由标志寄存器 eflags 中的 IOPL 位(bit12-bit13)和 TSS 中的 IO 位图决定的,它们用来指定执行IO 操作的最小特权级。I/O 相关的指令只有在当前特权级大于等于 IOPL 时才能执行。如果当前特权级小于 IOPL 时执行这些指令会引发处理器异常。这类指令有 in、out、cli、sti
- CPL 为 0 的时候 I/O 操作是不受限制的
- 做个小实验验证一下,我们在上一章节实现的 loader.asm 代码上 TASK A 中添加如下指令
mov al, 0x0A
out 0x20, al
- 不用在乎这个指令是什么意义,知道这是 I/O 操作就可以了
- 结果程序运行崩溃
... (0).[16768112] [0x00002065] 0007:0000002d (unk. ctxt): out 0x20, al ; e620 00016768112e[CPU0 ] exception(): 3rd (13) exception with no resolution, shutdown status is 00h, resetting 00016768112i[SYS ] bx_pc_system_c::Reset(HARDWARE) called 00016768112i[CPU0 ] cpu hardware reset
- 从打印提示中可以看出程序死在 out 指令处
- 那么,如何才能操作 eflags 的 IOPL 位呢?
- 可惜汇编中没有直接读写 eflags 寄存器的指令,不过可以使用如下指令迂回实现 eflags 读写操作
pushf pop eax or eax, 0x3000 ; bit12-bit13:11b push eax popf
- 解释一下,pushf 作用是将 eflags 寄存器入栈,再利用 pop eax 将刚刚入栈的 eflags 值弹出到 eax 寄存器中,然后改动 eax 的值,接下来 push eax 再将改动后的 eax 值入栈,最后再利用 popf 指令把改动过后的值弹出的 eflags 寄存器中
- 接下来就将上面的指令写到 TASK A 代码中,发现还是报错了,而且通过reg 指令发现 eflags 中 IOPL 位还是 0,并没有改变,这又是怎么回事呢?
- 只有在 0 特权下才能执行上面的指令。如果在其他特权级下执行此指令,处理器也不会引发异常,只是没任何反应
- 于是我们再把指令复制到 CODE32_START 代码段(特权级为 0)下,这回程序运行一切正常了。Ctrl+C 强制退出程序,再用 reg 命令查看 IOPL=3
- 最后提供一下实验完整代码:loader.asm