特权级由高到低转移

简介: 特权级由高到低转移

引言

  • 有了前面章节调用门和特权级的知识,现在我们来学习一下特权级如何转移
  • 处理器进入保护模式后,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...”
  • 很奇怪,不是说好的调用门可以实现不同特权级之间的跳转的吗?为啥上面的实验的结果并不像自己想象的那样跳转成功呢?
  • 很遗憾的告诉你,调用门是可以实现特权级跳转,但是只能实现从低特权级跳转到高特权级,不能实现高特权级到低特权级的跳转
  • 特权级从高到低跳转和从低到高跳转完全是两个不同的实现方式
  • 那么,改如何实现特权级由高到低的跳转呢?

特权级转移(高->低)

  • 解决方案
  1. 将指定目标的栈段选择子入栈
  2. 将指定目标的栈段栈顶位置入栈
  3. 将指定目标代码段选择子入栈
  4. 将指定目标代码段偏移地址入栈
  5. 远跳转: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 中。这就实现了跳转后自动切换栈的功能


目录
相关文章
|
6月前
|
安全
特权级由低到高转移
特权级由低到高转移
64 0
|
2月前
|
负载均衡 网络协议 Unix
Nginx负载均衡与故障转移实践
Nginx通过ngx_http_upstream_module模块实现负载均衡与故障转移,适用于多服务器环境。利用`upstream`与`server`指令定义后端服务器组,通过`proxy_pass`将请求代理至这些服务器,实现请求分发。Nginx还提供了多种负载均衡策略,如轮询、权重分配、IP哈希等,并支持自定义故障转移逻辑,确保系统稳定性和高可用性。示例配置展示了如何定义负载均衡设备及状态,并应用到具体server配置中。
|
3月前
|
运维 监控 定位技术
故障转移和自动恢复
故障转移和自动恢复
|
3月前
|
存储 缓存 运维
无状态故障转移与有状态故障转移
【8月更文挑战第24天】
30 0
|
4月前
|
消息中间件 运维 监控
中间件故障转移主-备配置
【7月更文挑战第25天】
37 2
|
4月前
|
运维 监控 Kubernetes
中间件故障转移自动切换
【7月更文挑战第25天】
40 2
|
4月前
|
运维 监控 算法
中间件故障转移故障检测
【7月更文挑战第25天】
45 1
|
6月前
|
安全
深入特权级转移
深入特权级转移
56 0
|
存储 运维 负载均衡
RH236配置IP故障转移--CTDB
RH236配置IP故障转移--CTDB
903 0
RH236配置IP故障转移--CTDB