引言
- 有了前面章节调用门和特权级的知识,现在我们来学习一下特权级如何转移
- 处理器进入保护模式后,CPL = 0(最高特权级),所以我们只能先实现高特权级到低特权级的跳转
- 在【调用门】 中提到过,调用门有个功能就是能够实现不同特权级代码之间的跳转
- 于是,先做个小实验,把 FUNC_DESC 描述符中特权级由 0 改为 3, 而 CODE32_START 和 TASK_A_CODE32_SEGMENT 都是通过调用门的方式调用 FUNCTION_SEGMENT 中的打印函数来实现打印功能
- 在上一章节实现的 loader.asm 代码上进行改动实验
- 代码的框架
boot.asm 跳转到 loader.asm,并打印 “Welcome to KOS.” | CODE16_START ; 实模式,打印 “Loader...” | CODE32_START ; 进入保护模式,通过调用门打印 “Enter protection” | TASK_A_CODE32_SEGMENT ; 跳转到局部代码段,通过调用门打印 “Task A”
- 改动处:
; FUNC_DESC : Descriptor 0, FUNC_SEG_LEN - 1, DA_C + DA_32 FUNC_DESC : Descriptor 0, FUNC_SEG_LEN - 1, DA_C + DA_32 + DA_DPL3 ; FUNC_SELECTOR equ (0x0006 << 3) + SA_RPL0 + SA_TIG FUNC_SELECTOR equ (0x0006 << 3) + SA_RPL3 + SA_TIG ; FUNC_PRINT_SELECTOR equ (0x0007 << 3) + SA_RPL0 + SA_TIG FUNC_PRINT_SELECTOR equ (0x0007 << 3) + SA_RPL3 + SA_TIG
- 运行一下,实验结果是只打印了 “Welcome to KOS.” 和 “Loader...”
- 很奇怪,不是说好的调用门可以实现不同特权级之间的跳转的吗?为啥上面的实验的结果并不像自己想象的那样跳转成功呢?
- 很遗憾的告诉你,调用门是可以实现特权级跳转,但是只能实现从低特权级跳转到高特权级,不能实现高特权级到低特权级的跳转
- 特权级从高到低跳转和从低到高跳转完全是两个不同的实现方式
- 那么,改如何实现特权级由高到低的跳转呢?
特权级转移(高->低)
- 解决方案
- 将指定目标的栈段选择子入栈
- 将指定目标的栈段栈顶位置入栈
- 将指定目标代码段选择子入栈
- 将指定目标代码段偏移地址入栈
- 远跳转:retf
- 先按照方案步骤实现代码,后面再具体分析
- 找到以前的代码 loader.asm,在这个基础上做来实现特权级由高到低的跳转实验
- 为啥要在这个代码基础上做实验?因为结构简单,容易看到现象
- 回顾代码的框架
boot.asm 跳转到 loader.asm,并打印 “Welcome to KOS.” | CODE16_START ; 实模式,打印 “Loader...” | CODE32_START ; 进入保护模式,打印 “Enter protection”
- 先上更改后的完整代码:loader.asm
- 给描述符增加特权级属性定义
; 段描述符中 DPL 属性定义(段描述符高 32 位的 bit13-bit14 ) DA_DPL0 equ 0x00 ; DPL = 0 DA_DPL1 equ 0x20 ; DPL = 1 DA_DPL2 equ 0x40 ; DPL = 2 DA_DPL3 equ 0x60 ; DPL = 3
- 现在我们先把保护模式下 32 位相关的代码段、数据段、栈段等相关特权级都改为 3
; 全局描述符表定义 ; CODE32_DESC : Descriptor 0, CODE32_SEG_LEN - 1, DA_C + DA_32 CODE32_DESC : Descriptor 0, CODE32_SEG_LEN - 1, DA_C + DA_32 + DA_DPL3 ; VIDEO_DESC : Descriptor 0xB8000, 0xBFFFF - 0xB8000, DA_DRWA + DA_32 VIDEO_DESC : Descriptor 0xB8000, 0xBFFFF - 0xB8000, DA_DRWA + DA_32 + DA_DPL3 ; DATA_DESC : Descriptor 0, DATA_SEG_LEN - 1, DA_DR + DA_32 DATA_DESC : Descriptor 0, DATA_SEG_LEN - 1, DA_DR + DA_32 + DA_DPL3 ; STACK32_DESC : Descriptor 0, TOP_OF_STACK32, DA_DRW + DA_32 STACK32_DESC : Descriptor 0, TOP_OF_STACK32, DA_DRW + DA_32 + DA_DPL3 ; 段选择符定义,RPL = 0; TI = 0 ; CODE32_SELECTOR equ (0x0001 << 3) + SA_RPL0 + SA_TIG CODE32_SELECTOR equ (0x0001 << 3) + SA_RPL3 + SA_TIG ; VIDEO_SELECTOR equ (0x0002 << 3) + SA_RPL0 + SA_TIG VIDEO_SELECTOR equ (0x0002 << 3) + SA_RPL3 + SA_TIG ; DATA_SELECTOR equ (0x0003 << 3) + SA_RPL0 + SA_TIG DATA_SELECTOR equ (0x0003 << 3) + SA_RPL3 + SA_TIG ; STACK32_SELECTOR equ (0x0004 << 3) + SA_RPL0 + SA_TIG STACK32_SELECTOR equ (0x0004 << 3) + SA_RPL3 + SA_TIG
- 然后我们按照上面的解决方案实现跳转
; jmp dword CODE32_SELECTOR:0 push STACK32_SELECTOR ; 将指定目标的栈段选择子入栈 push TOP_OF_STACK32 ; 将指定目标的栈段栈顶位置入栈 push CODE32_SELECTOR ; 将指定目标代码段选择子入栈 push 0 ; 将指定目标代码段偏移地址入栈 retf
- make 编译,运行一下,程序没有出任何错误,该打印的都打印出来了
- 让我们来深入看一下特权级是不是改变了吧
- 先进行反汇编指令,生成 loader.txt
ndisasm -o 0x900 loader.bin > loader.txt
- 找到 retf 指令对应的地址为 0x9A2
- 接下来打断点调试呗,使用 b 命令在 0x9a2 处打个断点,使用 c 命令运行到断点处后,使用 sreg 命令查看段寄存器内容,我们看到此时 CPL 的值也就是 cs 寄存器和 ss 寄存器的 bit0 - bit1 的值为 00b,说明此时程序当前特权级 CPL=0,然后再单步执行 s 命令后,再次使用 sreg 命令查看段寄存器内容,此时 cs 寄存器和 ss 寄存器的 bit0 - bit1 的值为 11b,即为 3,说明特权级成功由高(0)转变为低(3)了
00000000000i[ ] installing x module as the Bochs GUI 00000000000i[ ] using log file bochsout.txt Next at t=0 (0) [0xfffffff0] f000:fff0 (unk. ctxt): jmp far f000:e05b ; ea5be000f0 <bochs:1> b 0x9a2 <bochs:2> info b Num Type Disp Enb Address 1 pbreakpoint keep y 0x000009a2 <bochs:3> c (0) Breakpoint 1, 0x000009a2 in ?? () Next at t=16762538 (0) [0x000009a2] 0000:000009a2 (unk. ctxt): retf ; cb <bochs:4> sreg es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1 Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1 Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7 Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7 Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1 Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1 Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1 tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1 gdtr:base=0x00000902, limit=0x27 idtr:base=0x00000000, limit=0x3ff <bochs:5> s Next at t=16762539 (0) [0x000009d8] 000b:00000000 (unk. ctxt): mov ax, 0x0023 ; 66b82300 <bochs:6> sreg es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=0 cs:0x000b, dh=0x0040f900, dl=0x09d8005c, valid=1 Code segment, base=0x000009d8, limit=0x0000005c, Execute-Only, Accessed, 32-bit ss:0x0023, dh=0x0040f300, dl=0x0a460fff, valid=1 Data segment, base=0x00000a46, limit=0x00000fff, Read/Write, Accessed ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=0 fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=0 gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=0 ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1 tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1 gdtr:base=0x00000902, limit=0x27 idtr:base=0x00000000, limit=0x3ff
跳转详解
- 第一个感觉奇怪的地方就是学习8086汇编的时候通常 call、ret 成对出现,call far、retf 成对出现,现在却只有 retf,没有 call far,当然,在保护模式下也没有 call xxxSelector : offset 和 retf 成对出现
- 这就要从 call/call far 和 ret/retf 的本质说起了
- 通过学习 8086 汇编基础我们知道,8086 芯片程序是跟着 cs:ip 所指的位置执行的
- 先来看看近跳转 call 和 ret 的本质
- 用汇编来解释汇编
; call xxxFunc : 将程序运行的当前位置即 ip 压入栈中 ; 然后跳转到 xxxFunc 地址处执行 push ip jmp xxxFunc ; ret : ret 其实就是相当于将跳转到 xxxFunc 运行前的偏移地址 ip 的值再从栈中弹出到 ip 寄存器中 ; CPU 自动跟着 cs:ip 指定位置执行,所以程序就回到了跳转前的位置 pop ip
- 再看远跳转 call far 和 retf 的本质
; call far xxxFunc : 先将当前程序的段基址 cs 入栈 ; 再将当前程序偏移地址 ip 入栈 ; 最后再跳转到 xxxFunc 地址处执行 push cs push ip jmp xxxFunc ; retf : 先将跳转到 xxxFunc 运行前的段基址 cs 的值再从栈中弹出到 cs 寄存器中 ; 再将跳转到 xxxFunc 运行前的偏移地址 ip 的值再从栈中弹出到 ip 寄存器中 ; CPU 自动跟着 cs:ip 指定位置执行,所以程序就回到了跳转前的位置 pop ip pop cs
- 题外话:xxxFunc 中的入栈(push)和出栈(pop)必须成对出现,不然最后 ret/retf 的时候都不知道是什么值被写到 cs、ip 寄存器中了
- 远跳转比近跳转多了一个 cs 段寄存器的操作,所以所谓的近远的区分,不是跳转距离的远近,而是能否跳转到另一个代码段,谁能操作 cs,谁就能实现远段间跳转
- 当然,前面所说的是 8086 16位芯片的寄存器情况,这里的知识也是从 8086 汇编基础学习的笔记抄过来的,对于 32 位芯片,原理是一样的,只不过寄存器变成了扩展寄存器,比如 ip 变为了 eip
- call xxxSelector : offest 其实本质上 CPU 自带的保护机制检查和转换机制(CPU内部自动实现,参考调用门的章节学习),最终还是变成 call far xxxFunc
- 至此,实现跳转的五条指令中的后三条指令我们已经分析出其本质了,说白了最后 3 条指令其实就是为了改变 cs:eip 的值,哈哈哈,你肯定要说,那为啥不直接给 cs eip 这两个寄存器赋值,这只能说 CPU 不允许直接设置段寄存器,至于原因,那就要问芯片设计工程师了,我也不知道为啥
push CODE32_SELECTOR ; 将指定目标代码段选择子入栈 push 0 ; 将指定目标代码段偏移地址入栈 retf
- 还有开始的两条指令是干嘛的呢?
- CPU 在执行 retf 指令后,不光显性的执行了 pop eip 和 pop cs,还自动执行了 pop esp 和 pop ss 指令
- 当执行 pop esp 和 pop ss 指令前,程序 cs:eip 已经切换到新的段中了,而此时却在使用着老的 ss,esp,而不同的特权级必须使用不同的栈,所以 CPU 设计者就设计一种方式能够自动的切换栈,在跳转前最开始的两步将目标任务栈信息入栈操作,而等到跳转后就自动再把再把栈信息弹出到 ss,esp 中。这就实现了跳转后自动切换栈的功能