引言
- 直接以一个实验来引入本章节内容
实验目标
- 定义 32 位核心代码段和数据段特权级为 0
- 定义 32 为任务代码段和数据段特权级为 3
- 由核心代码段跳转到任务段(高->低)
- 在任务段中调用高特权级代码段打印字符串
- 打印完成后再由高特权级代码段返回任务段(低->高)
目标分析
- 像不像应用程序调用系统函数的情况?不是像,他就是系统调用的一种实现方法
- 用图形的形式更直观的理解
- 这上面这张图中,从右向左(高特权级->低特权级)的实现过程我们已经在 特权级转移(高->低) 中做过一定的讲解
- 而从左向右(低特权级->高特权级)的实现过程我们也在 调用门 中有了一定的了解,但是当时做的实验是相同特权级之间通过调用门实现跳转
- 现在特权级不同了,想要从低特权级跳转到高特权级代码段,还有一个知识点需要了解,那就是任务状态段 TSS
初识别任务状态段 TSS
- 英文全称:Task State Segment
- TSS 是处理器所提供的一种硬件数据结构,它保存了关键寄存器的值以及不同特权级使用的栈等数据
- 它是处理器在硬件上原生支持多任务的一种实现方式,也就是说处理器原本是想让操作系统开发厂商利用此结构实现多任务的,人家处理器厂商已经提供了多任务管理的解决方案,尽管后来操作系统并不买账,这是后话
- 不同的特权级需要使用不同的栈,每一个特权级对应一个私有栈
- 使用 TSS 数据结构,处理器可以从一个任务切换到另一个任务,同时保存原任务的上下文,等状态切换回来的时候再恢复到切换前的状态
- 32 位任务状态段 TSS 格式
特权级转移时 TSS 中栈的变化
- 首先看上图中看一下 TSS 中各特权级栈的信息
- 特权级 0 :SS0, ESP0
- 特权级 1 :SS1, ESP1
- 特权级 2 :SS2, ESP2
- 低特权级->高特权级(调用门)
- 从 TSS 中获取高特权级目标栈段
- 将低特权级栈信息压入高特权级栈中(ss 和 esp)
- 高特权级->低特权级(retf)
- 将低特权级栈信息从高特权级栈中取出并恢复到 ss 和 esp 寄存器中
有趣的问题
- 问题 1:为啥不同的特权级要使用不同的栈?
- 要是不同的特权级可以使用相同的栈,那么应用程序(特权级 3)对栈中数据的修改是不是会影响内核(特权级 0)的运行了。要是应用程序故意搞破坏呢,还不把系统给干崩了啊,操作系统怎么可能会留这么到的漏洞呢
- 问题 2:TSS 中为什么只保存 3 个特权级(0、1、2)的栈信息?
- 当低特权级 3 跳转到高特权级 0 或 1 或 2 时,特权级 3 的栈信息 ss3 和 esp3 会被压入对应的高特权级栈中保存,而当从高特权级 0 或 1 或 2 跳回低特权级 3 时,原先被保存的 ss3 和 esp3 又会被从高特权级栈中取出恢复,既然 ss3 和 esp3 会压栈到高特权级的栈中保存,那么完全没必要再重复指定一个保存 ss3 和 esp3 的地方,想要的时候直接从高特权级栈中出栈即可
- 问题 3:虽然 Intel 专门为进程切换提供了 TSS 一系列解决方案,但是各家操作系统并不买账,没人用(仅使用其中的 ss0 和 esp0),原因有两个
- 一是操作系统厂商并不希望把自己的产品绑定在 Intel 处理器上,所有的操作系统都会考虑,如果以后有新的处理器出来,操作系统要能够运行在新的处理器上
- 二是 Intel 提供的方案其进程切换效率比较低
准备工作
- 在之前章节中实现的代码 loader.asm 基础上进行改动实验
- 先来回顾一下这个基础代码的框架
boot.asm 跳转到 loader.asm,并打印 “Welcome to KOS.” | CODE16_START ; 实模式,打印 “Loader...” | CODE32_START ; 进入保护模式,通过调用门打印 “Enter protection” | TASK_A_CODE32_SEGMENT ; 跳转到局部代码段,通过调用门打印 “Task A”
- 先上实验成功后的完整代码:loader.asm
- 我们先把 Task A 相关特权级改为 3,用来模拟低特权级的应用程序
; 局部段描述符 TASK_A_CODE32_DESC : Descriptor 0, TASK_A_CODE32_SEG_LEN - 1, DA_C + DA_32 + DA_DPL3 TASK_A_DATA32_DESC : Descriptor 0, TASK_A_DATA32_SEG_LEN - 1, DA_DR + DA_32 + DA_DPL3 TASK_A_STACK32_DESC : Descriptor 0, TASK_A_STACK32_SEG_LEN - 1, DA_DRW + DA_32 + DA_DPL3 ; 局部段选择子 TASK_A_CODE32_SELECTOR equ (0x0000 << 3) + SA_RPL3 + SA_TIL TASK_A_DATA32_SELECTOR equ (0x0001 << 3) + SA_RPL3 + SA_TIL TASK_A_STACK32_SELECTOR equ (0x0002 << 3) + SA_RPL3 + SA_TIL
- 有一个地方还需要改一下,那就是调用门要被低特权级 3 所调用,那么调用门的特权级也要改为 3
; 门描述符
FUNC_PRINT_DESC : Gate FUNC_SELECTOR, printOffset, 0, DA_CALL_GATE + DA_DPL3
; 选择子
FUNC_PRINT_SELECTOR equ (0x0007 << 3) + SA_RPL3 + SA_TIG
实验开始
- 首先程序需要从核心代码段 CODE32_START (特权级 0)跳转到任务Task A (特权级 3)
; jmp TASK_A_CODE32_SELECTOR:0 push TASK_A_STACK32_SELECTOR push TASK_A_TOP_OF_STACK32 push TASK_A_CODE32_SELECTOR push 0 retf
- 原理这里就不再说明了,可以参考 特权级转移(高->低)
- 接下来实现低特权级代码段跳转到高特权级代码段(任务 Task A 调用全局函数段 FUNCTION_SEGMENT 中的打印函数 print_str_32)
- 前面刚说过,想要从低特权级跳转到高特权级代码段,不光需要调用门,还需要任务状态段 TSS
- 那么这个 TSS 是如何使用的呢?
- 首先我们先定义出 TSS 数据结构(参照上面 32 位任务状态段 TSS 格式示意图)
TSS_SEGMENT: dd 0 dd TOP_OF_STACK32 ; esp0 dd STACK32_SELECTOR ; ss0 dd 0 ; esp1 dd 0 ; ss1 dd 0 ; esp2 dd 0 ; ss2 times 4 * 18 dd 0 dw 0 dw $ - TSS_SEGMENT + 2 db 0xFF TSS_LEN equ $ - TSS_SEGMENT
- 在这个实验中我们只用到了 esp0 和 ss0,所以这两个值要条充好
- 其余用不到的暂时先填充数据 0
- 最后 3 个字节数据暂时先不深究,先这么写,后面再做讲解
- 定义好了这个数据结构接下来怎么办呢?
- 老套路了,先将其加载到处理器 (ltr)
mov ax, TSS_SELECTOR ltr ax
- 再定义对应的描述符和选择子
; 描述符 TSDESC : Descriptor 0, TSS_LEN -1, DA_TSS + DA_DPL0 ; 选择子 TSS_SELECTOR equ (0x0007 << 3) + SA_RPL0 + SA_TIG
- 别忘了初始化一下段基址
mov esi, TSS_SEGMENT mov edi, TSDESC call InitDescItem
- 好了,改动完毕,运行一下试试
- 结果,只打印了 “Welcome to KOS.” 、“Loader...”、“Enter protection”,并没有打印出 “Task A” ,程序崩溃了
- 没有什么好办法,只能反汇编后断点调试了
- 经过长时间摸索,发现程序每次运行到 mov [gs:edi], ax 这条指令后就会崩溃
- 看一下这条指令是干嘛,这条指令是将 Task A 中数据拷贝到显存里去,仔细推敲一下,显存的特权级 DPL 为 0,你能把特权级 3 的数据拷贝到特权级 0 的段中吗?处理器显然不允许你那么干
- 于是,我们把显存段特权级也改为 3,果然成功打印出了 “Task A”
; VIDEO_DESC : Descriptor 0xB8000, 0xBFFFF - 0xB8000, DA_DRWA + DA_32
VIDEO_DESC : Descriptor 0xB8000, 0xBFFFF - 0xB8000, DA_DRWA + DA_32 + DA_DPL3
- 那么,我们把显存段特权级改为 3 合适吗?
- 显存段唯一的作用就是显示,就算应用程序胡乱往显存里放数据,顶多画面显示异常,并不会影响处理器功能的正常执行。所以,我们把显存段特权级改为 3 也是合理的