特权级由高到低转移

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

引言

  • 有了前面章节调用门和特权级的知识,现在我们来学习一下特权级如何转移
  • 处理器进入保护模式后,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 中。这就实现了跳转后自动切换栈的功能


目录
相关文章
|
存储 Java 测试技术
《Spring 测试指南》:JPA、MockMvc 和 @SpringBootTest 详解
Spring 提供了一组测试工具,可以轻松地测试 Spring 应用程序的各个组件,包括控制器、服务、存储库和其他组件。它具有丰富的测试注释、实用程序类和其他功能,以帮助进行单元测试、集成测试等。
211 0
|
算法
【MATLAB】逐次变分模态分解SVMD信号分解算法
【MATLAB】逐次变分模态分解SVMD信号分解算法
922 0
|
SQL 缓存 监控
SpringBoot整合阿里巴巴Druid数据源
Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。 Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。 Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。 本文主要讲解如何整合Druid数据源及Druid常用配置项和详解
5811 1
SpringBoot整合阿里巴巴Druid数据源
|
11月前
|
Web App开发 安全 Java
Debian 12.7 推出安全性和稳定性改进
【10月更文挑战第16天】
438 3
Debian 12.7 推出安全性和稳定性改进
|
10月前
|
前端开发 JavaScript API
前端界的革命性突破:掌握这些新技术,让你的作品引领潮流!
【10月更文挑战第30天】前端技术日新月异,从传统的HTML、CSS、JavaScript到现代的React、Vue、Angular等框架,以及Webpack、Sass等工具,前端开发经历了巨大变革。本文通过对比新旧技术,展示如何高效掌握这些新技术,助你作品引领潮流。
178 2
makefile 变量的替换,嵌套引用,命令行变量
makefile 变量的替换,嵌套引用,命令行变量
296 1
|
存储 Linux 数据处理
探索Linux操作系统的内核与文件系统
本文深入探讨了Linux操作系统的核心组件,包括其独特的内核结构和灵活的文件系统。文章首先概述了Linux内核的主要功能和架构,接着详细分析了文件系统的工作原理以及它如何支持数据存储和检索。通过比较不同的文件系统类型,本文旨在为读者提供一个关于如何根据特定需求选择合适文件系统的参考框架。
|
10月前
|
存储 监控 供应链
深入理解操作系统:从理论到实践
本文将深入探讨操作系统的基本概念、功能和结构,以及它们如何影响计算机系统的性能和稳定性。我们将通过实例和代码示例来揭示操作系统的工作原理,并讨论其在现代计算环境中的重要性。无论你是计算机专业的学生,还是对操作系统感兴趣的技术爱好者,这篇文章都将为你提供有价值的见解和知识。
|
Java 数据库连接 Maven
【Spring】掌握 Spring Validation 数据校验
【Spring】掌握 Spring Validation 数据校验
416 0
|
小程序 Android开发 开发者
1. 建立 HBuilder 与 微信开发者的联系
1. 建立 HBuilder 与 微信开发者的联系
137 0