- RPL的作用与ARPL的使用
- 对数据段特权检查
- 对直接转移的代码段特权检查
- 栈段的检查
- 调用门的检查
- 栈切换
- * 模拟从高特权到低特权的转移方法
- 栈切换涉及到的TSS
- 切换任务TSS
- 切换任务伪代码
- 半自动切换任务伪代码
- 特权级转移完整代码
- 任务切换使用call完整代码
- 任务切换使用jmp的半自动切换完整代码
权限问题:
- 由于CPL,DPL 无法完整表达权限的问题.
- 注: RPL仅是一个参与判断权限的参数,别想复杂了,跟函数里多增一个参数用于判断类似
- 因此加入RPL.此参数由操作系统来保证,CPU仅使用.
-
- cpu执行的代码都将由CS所指向,因此代码段的特权级是特权的本质,数据段的特权只是设置一个门槛
-
- DPL:访问此描述符的权限
-
- RPL: 想以哪种权限去访问, 操作系统来填写, 一个自由的参数, CPU根据此参数去判断
-
- CPL: 当前正在运行的代码段的权限 (也就是代码段描述符里的DPL, 换个了名字.比如有2个代码段,其中一个在运行,正在运行的那个代码段的权限也叫 当前特权级别)
-
- 代码段描述符里的DPL=CPL=CS选择子中的RPL(16位中的低2位). (特例是跳转到一致性代码段,CPL不变,这部分在下面)
-
- 代码段特权级的改变,会连着栈的特权一起切换,代码段里的call,ret,retf等一系列指令都需要栈的参与(这部分下面有)
- 上面虽然一下子多出了什么RPL,CPL 这些概念. 通过伪代码看一下就知道了:
; 假设0x08 段选择子的描述符是一个0x7c00基址的512字节32位代码段
mov eax,cr0
or eax,1
mov cr0,eax ;开启了PE
;接下来就需要转移到 保护模式的代码中
jmp dword 0x08: into_protected_code
;0x08段选择子的 RPL=0
[bits 32]
into_protected_code:
;当转移过来后, cs=0x08, 当前特权级CPL = cs.rpl(0x08的低2位)
;同时 当前特权级cpl = 0x08指向的代码段描述符的DPL
-
- 为什么需要权限, 下面伪代码示例:
- 权限示例1:
;假设当前是用户程序,并且猜到了 0x38是系统数据段选择子, 此描述符的DPL=0
;0x38 = 111000B , 也就是GDT中的索引7的描述符,RPL=0
;当前CPL=3,用户特权
mov eax,0x38
mov ds,eax ; CPU将在这做特权检查,如果没有检查,用户程序将把系统数据段破坏
; 现在有了检查,当前CPL=3. 想访问0x38指向的数据段,需要比较
; 0x38描述符中的DPL : CPL <= 0x38描述符DPL && RPL <= 0x38描述符DPL
; 一旦不符合规则,CPU引发异常
; 这里RPL并没有发挥作用,第一个条件CPL <= DPL( 3 <= 0 ) 就没有通过
; RPL发挥作用的是在低特权调用高特权代码的时候
mov edi,0
mov ecx,100
.fuck_sys_data:
mov dword [ds:edi],0
add edi,4
loop .fuck_sys_data
;特权指令lgdt , 如果用户也能用的话, 可以把GDT都全部改写
lgdt [0x....]
权限示例2, RPL的作用,依旧是伪代码
: ;用户程序通过调用门 调用到特权0的print过程
;调用门DPL=3,因此用户程序可以访问
;print 参数: 用户数据段选择子, 用户数据段偏移地址, 字符串长度
;print 会根据 段选择子:偏移地址 逐个输出字符
;假设用户数据段选择子: 101111B = 0x2F , TI=1, 最后2位:11 , RPL=3,这是正常的
;用户数据段DPL=3
;假设调用门的段选择子: 0x80
;调用门,将复制用户栈中的参数到特权0的栈中,下面有说
;用户代码段 ;特权0代码段
;--------------------------正常的调用----------------------
;ds:0x2F
push dword 10 ;长度 | print: (push ebp,mov ebp,esp 省略)
push msg ;偏移 | mov eax,[ebp + 12] ;用户数据段选择子
push ds ;数据段段选择子 | mov ds,eax
call 0x80:0x00 ;通过调用门调用print | mov ebx,[ebp + 16] ;偏移
mov ecx,[ebp + 20] ;长度
;之后输出过程省略
retf 12 ;3个参数 * 4字节
;在这里用户程序非常守规矩,老老实实把自己的数据段选择子(ds)给了特权0
;特权0代码段也没做任何检查,直接使用了 用户的ds
;此时特权0: CPL <= 用户数据段DPL && RPL(也就是eax中的低2位) <= 用户数据段DPL .
; 0 <= 3 && 3 <= 3
;因此可以访问此用户数据段. 一切正常
;--------------------------正常的调用结束----------------------
;现在用户修改了自己RPL, 不再直接 push DS
;用户数据段DPL=3
;原来的用户数据段选择子 : 0x2f = 101111B , 修改成 101100B = 0x2c
;RPL修改成00, 说明是通过特权0去访问的
;这例子说明RPL可以由用户随意更改
;用户代码段 ;特权0代码段
;------------------------- 不正常的调用1----------------------------
xor eax,eax
mov ax,ds
and ax,0xFFFC ;把最后2位 改成0 => 101100B
;用户随意修改了RPL
push dword 10 ;长度 | print: (push ebp,mov ebp,esp 省略)
push msg ;偏移 | mov eax,[ebp + 12] ;用户数据段选择子
push eax ;段选择子 | mov ds,eax
call 0x80:0x00 ;通过调用门调用print | mov ebx,[ebp + 16] ;偏移
mov ecx,[ebp + 20] ;长度
;之后输出过程省略
retf 12
;此时, 特权0中 : CPL <= 用户数据段DPL && RPL <= 用户数据段DPL
; 0 <= 3 && 0 <= 3
;虽然此时有点不正常, 但至少可以访问
;------------------------- 不正常的调用1结束----------------------------
;现在用户通过一些手段,得知 特权0的数据段段选择子是:0x28 = 101000B
;特权0数据段DPL=0
;如果没有RPL的限制, 用户将直接可以访问特权数据段
;用户代码段 ;特权0代码段
;------------------------- 不正常的调用2 ----------------------------
mov eax, 0x28 ; 特权数据段选择子
push dword 10 ;长度 | print: (push ebp,mov ebp,esp 省略)
push 0 ;偏移 | mov eax,[ebp + 12] ;数据段选择子
push eax ;段选择子 | mov ds,eax
call 0x80:0x00 ;通过调用门调用print | mov ebx,[ebp + 16] ;偏移
mov ecx,[ebp + 20] ;长度
;之后输出过程省略
retf 12
;特权0中 : CPL <= 特权数据段DPL && RPL <= 特权数据段DPL
; 0 <= 0 && 0 <= 0
;按权限规则, 一切OK, 用户程序将可以直接把 特权0的数据段 打印出来
;异常情况:
;如果把0x28的低2位改成11B : 101011B . 那么在特权0中将引发异常. (通过特权3去访问)
; RPL <= 数据段DPL( 3 <= 0 ) 此条件不被满足
;------------------------- 不正常的调用2结束----------------------------
; 现在通过 ARPL( adjust RPL ) 指令 来限制用户随意修改RPL, 由内核过程自己保证特权问题
; 通过调用门转移到print时, 特权0的栈:
; [高地址->低地址]
; 用户ss , 用户esp, 参数[字符串长度,偏移,段选择子], 用户cs, 用户eip
; 对栈中数据有疑问的可以查看 "栈切换" 部分
; arpl 将修改用户传递过来的数据段段选择子的RPL部分 (低2位)
; arpl 目的操作数, 源操作数
; arpl 比较 2个操作数的低2位,如果目的操作数RPL < 源操作数的RPL 则把目的操作数RPL修改成源操作数RPL
; 下面例子 arpl bx,ax => arpl 0x28, cs 比较RPL=> arpl 00(RPL),11(RPL)
; 由于目的操作数 0x28 的RPL < 源cs的RPL , 因此最后 : bx = 101011B
;用户代码段 ;特权0代码段
;-----------------------------ARPL 指令--------------------------------
;0x28 的RPL:00
mov eax, 0x28 ; 特权数据段选择子
push dword 10 ;长度 | print:
push 0 ;偏移 | push ebp
push eax ;段选择子 | mov ebp , esp
call 0x80:0x00 ;通过调用门调用print | mov eax,[ebp + 8] ;用户cs
| mov ebx,[ebp + 12] ;数据段选择子
| arpl bx,ax
; 此时 数据段选择子 RPL 被修改成用户特权3
|
| mov ds,ebx ;引发异常
| mov ebx,[ebp + 16] ;偏移
mov ecx,[ebp + 20] ;长度
;之后输出过程省略
retf 12
; arpl 把源RPL(00) 修改成了用户特权(11)
; 此时 ebx = 101011B = 0x2B
; 当 mov ds, ebx 时, 检查访问权限 :
; 1. CPL <= 数据段DPL => 0 <= 0 , 通过
; 2. RPL <= 数据段DPL => 3 <= 0 , 不通过, 引发异常
;-----------------------------ARPL 指令结束--------------------------------
对于数据段的特权检查:
从大方向说:只要权限比数据段大 ,或者相等就OK
实际根据以下几个步骤:
CPL:当前CS段的RPL, RPL:请求这个数据段的权限(自由参数,由操作系统自己决定), DPL: 数据段的权限
-
- 根据CPL, RPL , DPL 这3个来检查
-
- CPL <= DPL && RPL <= DPL
假设有 数据段 DPL= 2:
代码段CPL=0 | 代码段CPL=1 | 代码段CPL=2 | 代码段CPL=3 |
---|---|---|---|
RPL=0 可访问 | RPL=1可访问 | RPL=2可访问 | RPL=3 不可访问 |
RPL=1或2 可访问 | RPL=0 可访问 | RPL=1 可访问 | RPL=2 不可访问 |
RPL=3 不可访问 | RPL=2可访问 | RPL=0可访问 | RPL=1不可访问 |
- 可以看到CPL=0的代码段,即使有最高权限,但如果RPL=3,也无法访问
- 至于CPL=3的代码段,无论如何都无法访问,毕竟CPL>DPL
对跳转或调用的代码段检查
这里特指 call far , jmp far
- jmp , call ,ret 这些都是段内的, 不需要重新加载cs , 因此不做检查
-
- 调用过程的CPL,RPL
-
- 转移到此处的目标描述符的DPL,C(是否是一致性代码段)
- 综上CPL,RPL,DPL,C 4项参与检查
- 又根据C 进行分别检查 if ( 1==C ){ 一致性代码段检查} else { 非一致性代码段检查}
如果C=0, 非一致性代码段的检查:
- CPL == DPL , 必须是相同特权级, 否则产生异常
- 由于只能是平级跳转,因此转移前后CPL不变
- 对于RPL: RPL<=CPL即可,毕竟RPL是一种希望用哪种方式(权限)去访问,RPL并不会影响CPL,RPL只用于检查
假设非一致性代码段的描述符:DPL=2,C=0
非一致性代码段描述符 | 代码段CPL=1 | 代码段CPL=2 | 代码段CPL=3 |
---|---|---|---|
现有非一致性代码段的描述符:DPL=2,C=0 | CPL不一致,RPL=0~3都不可转移 | RPL=0~2都可以转移,RPL=3则无法转移 | CPL不一致,RPL=0~3都无法转移 |
- | - | 成功转移后,CPL不变,RPL只用作检查,即使rpl != CPL,转移后CPL也不变 | - |
C=1, 一致性代码段检查(依从性)
- RPL不参与检查
- 满足: CPL>=DPL(一致性代码段), 也就是当前特权比目标代码段低或相等即可
- 一旦转移后, CPL不改变,依旧保持之前调用者的
假设一致性代码段的描述符:DPL=1,C=1
一致性代码段描述符 | 代码段CPL=1 | 代码段CPL=2 | 代码段CPL=3 | 代码段CPL=0 |
---|---|---|---|---|
一致性代码段的描述符:DPL=1,C=1 | 可以访问 | 可以访问 | 可以访问 | 不可以访问 |
- RPL不参与检查,转移成功后CPL不改变
综上
- 对于代码段, 主要还是看CPL
- 对于数据段,需要RPL来辅助检查
栈段检查
- CPL=RPL=DPL
调用门检查
- 通过调用门可以执行一个高于本CPL的过程
- 调用门指向了某个代码段内的某一个过程
- 调用门是一个描述符(8个字节),S=0 ,意味着这是一个系统段描述符, 可以存放在GDT或LDT中
- 调用门本身也有DPL,想要使用调用门,调用者的CPL<=调用门DPL,也就需要达到使用调用门的权限
- 访问调用门可以使用 jmp , call
- 一旦访问了调用门,检查顺利后,根据调用门描述符内的 段选择子获取段描述符的基址 + 调用门内的偏移地址,就这个过程的线性地址
- 调用门是一个系统段描述符(S=0,TYPE=1100),格式:
31 ~ 16 | 15 | 14 ~ 13 | 12 | 11 ~ 8 | 7 ~ 5 | 4 ~ 0 |
---|---|---|---|---|---|---|
段内偏移高16位 | P | DPL | 0 | TYPE(1100) | 000 | 参数个数 |
31 ~ 16 | 15 ~ 0 |
---|---|
段选择子 | 段内偏移低16位 |
- 调用门需要4项检查:
- 当前调用者的CPL
- 调用门选择子RPL (操作系统 自己维护)
- 调用门描述符DPL
- 目标代码段描述符的DPL
- 检查2步:
-
- 首先要满足 本身能够访问调用门:CPL <= 门DPL, RPL <= 门DPL
-
- 下表格:
指令 | 一致性代码段 | 非一致性代码段 |
---|---|---|
call | 代码段描述符DPL<=CPL | 代码段描述符DPL<=CPL |
jmp | 代码段描述符DPL<=CPL | 代码段描述符DPL=CPL |
call 指令,栈切换 | CPl不发生变化,栈不切换 | CPL变成目标代码段的DPL,栈需要切换 |
jmp 指令,栈切换 | CPL不变,栈不切换 | 由于DPL=CPL,栈不切换 |
- 再一次的强调,权限无法从高到低
- 只有CPL变了,栈才会变,因此上面只有当call指令调用 非一致性代码段的时候, 才会切换栈, 除非CPL=DPL(例如CPL=0,目标代码段描述符的DPL=0)
- 综上:
- 对于一致性代码段: CPL>=目标代码段DPL, 不论JMP,CALL, 转移后CPL不变,栈不变
- 对于非一致性代码段:
-
- CALL指令要求: CPL >= 目标代码段DPL , CPL变成目标代码段DPL, 切换栈(除非CPL=DPL,那么栈不变)
-
- JMP 指令: CPL = 目标代码段DPL , CPL不变,栈不变
- 看上去内容有点多,需要到处判断,一会门检查,一会代码段DPL检查,还要考虑栈切换的问题
- 实际没那么麻烦,也就2条需要注意的
-
- 一个是有权限访问门 ,也就是本身权限至少要与门相等 数值上: CPL <= 门DPL, RPL <=门DPL
-
- 调用者代码段的权限,要低于代码段或相等,也就是低权限到高权限,那么数值上: CPL>= 目标代码段DPL
- jmp 不涉及栈的问题, call 会 push cs push eip 因此会改变栈
- 至于jmp 和 call 的区别不用记, 让CPU产生保护性异常(GP)告诉我们即可,一旦产生异常说明此处jmp 指令有问题了( CPL != DPL)
- 对于栈的问题,一句话就解决,CPL一变, 栈跟着变 ; CPL不变,栈也不变
调用门的首次检查例子:
门描述符 | 代码段A | 代码段B | 代码段C | 代码段D | - |
---|---|---|---|---|---|
门DPL=3 | CPL=3 | CPL=2 | CPL=1 | CPL=0 | 代码段A,B,C,D任意+RPL=0~3都可以访问 |
门DPL=2 | CPL=3,RPL=0~3都无法访问 | CPL=2,RPL=0~2可以访问,RPL=3无法访问 | CPL=1,RPL=0~2可以访问,RPL=3无法访问 | CPL=0,RPL=0~2可以访问,RPL=3无法访问 | A的CPL权限不足以访问门DPL;B,C,D的CPL全部满足,同时:RPL<=门DPL |
- 调用门使用例子:
;假设调用门段选择子 : 0x20 , 存放在GDT中
func dd print ;偏移, 即使是0也OK
dw 0x20 ;门选择子
call far [func] ;使用调用门
call 0x20:0x0000 ;也可以这样
; 由于是调用门选择子 , 因此将忽略偏移地址. 门描述符中已经有了偏移地址和目标段选择子
; 如果一切检查通过后 ,伪代码 :
mov cs, 0x20 描述符中的段选择子
mov eip, 0x20 描述符中的偏移地址
栈切换
-
栈切换与TSS相关,TSS(任务状态段),TSS保存了当前任务所有寄存器,以及特权0,1,2栈(这些栈需要由操作系统自己创建,CPU帮你使用); TSS在下面有讲
-
用于call 指令的调用门, 只有call 指令对非一致代码段会改变CPL. 因此栈会切换
-
对于用户层:CPL=3. 一般情况下定义额外3个栈,分别对应特权0,1,2.
-
用户层自己的栈放在ss,esp,因此不需要额外定义
-
如果用户层能确定使用的调用门只使用特权0的过程,那么可以只额外定义特权0的栈,也就是用户层自己一个栈,特权0一个栈
-
切换栈的流程:
-
- 目标代码段(被调用代码段) 会根据自己的DPL.从TSS中选择一个当前特权的栈
-
- 由于执行完后将返回到之前的代码段中, 因此将先临时保存调用者的SS,esp
-
- 把TSS中对应特权的栈给:ss,esp赋值
-
- 把临时保存的SS,ESP, push 到特权栈中
-
- 根据调用门描述符中的参数个数, 把调用者栈中的参数复制到 特权栈; 如有3个参数,则复制12字节(3*4, 以32位调用门为例, 如果是16位调用门则每个参数2个字节)
-
- 把调用者的cs,eip ,push 到特权栈,用于之后返回
-
- 把调用门中的段选择子,加载到cs, eip = 偏移地址. 开始执行
-
第6步 :
-
- 跟段间调用的流程类似, 只是不在自己的栈中push, 而是在对应的特权栈中push.
-
- 更准确的说,如果是平级的:当前CPL=目标DPL,则push到当前栈中(栈不变), 否则push到特权栈中
-
调用者的栈 | 被调用者,特权栈(此栈的段选择子在TSS中) |
---|---|
参数3 | 调用者ss |
参数2 | 调用者ESP位置 |
参数1,esp位置 | 参数3 |
- | 参数2 |
- | 参数1 |
- | 调用者cs |
- | 调用者eip |
返回问题
- 同一特权级别的 近返回 (ret ) 仅会检查界限问题, 再次强调同一特权不涉及栈切换
- 对于特权改变的远返回, 只能返回特权小的代码段
- 根据特权栈中的cs段选择子的低2位(RPL)来判定:
-
- 如果 当前CPL = 目标cs.RPL 说明是同一级别的段间远返回 (pop eip , pop cs)
-
- 当前CPL < 目标CS.RPL 也就是 当前CPL <目标DPL ,这是特权需要改变的远返回(需要pop eip,pop cs,pop esp, pop ss)
-
- 例如当前CPL=0, 目标(DPL)CS.RPL=3.这符合条件
- 假设当前是用户程序CPL=3,特权0的被调用过程,有3个参数被传递进来(通过调用门),结尾处: retf 12,返回流程:
-
- 首先特权0栈:pop eip,pop cs
-
- 当前有3个参数,每个参数4个字节,结尾是retf 12. 需要回收特权0栈的参数空间: esp + 12
-
- 参数上面是用户的ss,esp,因此:pop esp,pop ss
-
- 用户esp+12.还原用户栈空间.(特权0栈的参数是从用户栈中复制的)
-
- 此时,cs,eip,ss,esp 这4项全部还原. 同时检查其余段寄存器:ds,es,gs,fs 如果指向的段描述符权限比CPL高(CPL > 段DPL),则把对应段寄存器置0,例如: mov ds,0
- 因为如果特权0代码段中如果使用了DPL=0的ds,但忘了还原ds(一开始没有push ds, 最后没有pop ds).回到用户代码段时ds还指向DPL=0的描述符.
- 特权检查只在 mov ds,0xXXX 这个时候(这在特权0中操作的). 回到用户程序后,用户可以直接访问特权0的数据段
模拟高特权转移到低特权的方式
- 由于特权从方式(call 调用门,改变CPL)上来说只能从低到高
- 可以在 返回的时候 做手脚, 根据上面的 “返回问题”. 可知返回根据 栈中的cs.rpl来判断
-
- 如果 ( 栈中cs.rpl = 当前cpl ) 则是段间返回 (retf => pop eip pop cs)
-
- 如果 (栈中cs.rpl > 当前cpl ) 则是特权改变的远返回,这里就是做手脚的地方
-
- 特权远返回对应指令: retf => pop eip pop cs ,esp + (参数个数)* 4或2 , pop esp pop ss
- 想使用特权改变的远返回来从高到低转移, 需要在栈中: push 用户ss,push 用户esp, (push 参数)(根据想返回的过程添加), push cs, push eip
- push指令使用的栈DPL必须是与当前CPL相等的栈( 栈DPL = 当前CPL), 或者也可以直接使用tss段中栈(栈DPL=当前cpl)
- 具体伪代码:
;假设 当前是特权0, ss_cpl_0 是特权0 的栈; ss 也可以使用tss中的特权0栈
mov eax, ss_cpl_0
mov ss,eax
mov esp, 0
;假设用户过程没有参数. 如有参数则需要添加
push dword 用户ss
push dword 用户esp
;push dword 参数1 ; 如用户过程有参数必须添加
;push dword 参数2
push dword 用户cs
push dword 用户过程偏移地址
;通过retf . 比较 当前cpl < 栈中用户cs.rpl 是否成立, 如果是,则是特权级改变远跳转
retf ; retf 8 : 假设用户有2个参数
TSS(任务状态段) 以及TSS描述符
- TSS不仅用于多个任务的切换,也用于特权栈的切换
- 就上面的栈切换来说,一旦CPL改变,就需要从任务对应的TSS中获取特权栈(1个任务对应1个TSS)
- TSS中包含了当前任务的所有寄存器,以及特权0,1,2的栈,这些栈需要我们自己创建
- TSS的任务切换:
- 一旦任务切换,TSS保存当前任务的所有寄存器(为了下一次运行的恢复). 下一个任务将从自己的TSS中恢复所有的寄存器,然后此任务从新加载的CS.eip开始继续执行
- TSS本身一个结构,为了使用TSS,必须为其创建描述符. TSS描述符跟调用门描述符都是一种系统段描述符(S=0),但TYPE不同
- TSS的TYPE:1001 (非活动),TYPE:1011(繁忙), 繁忙标志是为了不让自己调用自己,防止递归(重入)
- TSS系统段描述符和LDT系统段描述符只能存放在GDT中
- TSS描述符的DPL可自定义
- 只要当前CPL <= TSS描述符的DPL,就可以访问此描述符,这意味着只要 CPL<=TSS.DPL 就可以调度任务
- 关于LDT:(局部描述符表)
-
- LDT 用于为每个任务存放各种描述符的地方( 代码段 数据段), 主要是为了能与GDT中其他描述符隔离开,便于管理, 比如A,B,C,D 4个任务, 他们的各种描述符都放在自己的LDT中
-
- CPU 厂商只是建议为每个任务 分配一个LDT一个TSS 来管理并运行一个任务,不代表这是一种强制
- 真正影响任务的是TSS,任务切换只与TSS有关. 至于LDT,取决于你自己
- 综上:
- LDT:局部描述符表. 类似GDT,用于存放描述符. 每个任务的段描述符分开管理,让任务的段描述符都在自己的LDT中
- TSS是一种结构,用来表示一个任务,用于任务的切换,也用于特权栈的切换(低特权到高特权的代码段运行)
- 要使用TSS,必须为TSS创建描述符,且必须存放在GDT中
- 要使用LDT,必须为LDT创建描述符,且必须存放在GDT中. LDT 跟 GDT一样需要 16位界限,32位基址.但这些属性存放在描述符里,不像GDT直接加载, 加载LDT的指令:lldt ldt段选择子
- TSS的对应寄存器是TR, 使用LTR指令可加载到TR寄存器中, 其TYPE从1001变成1011(繁忙状态), 但并不会切换任务
-
ltr TSS选择子 => 将修改TSS描述符中的B位
- 在CPU眼里, TR不停的变换TSS,就是任务的切换了
- LDT对应的寄存器是LDTR, 使用LLDT指令可加载到LDTR寄存器中
- LTR TSS段选择子 => TR=TSS段选择子
- LLDT LDT段选择子 => LDTR=LDT段选择子 ; 一旦LLDT加载完成, 局部描述符表生效, 可访问LDT中的描述符了
任务切换tss
- 只要当前CPL <= TSS描述符DPL && TSS选择子RPL <= TSS描述符DPL (与数据段检查一致),就可以调度此任务
- TSS描述符的B位 用于检测防止重入(递归调用), 如果一个任务描述符的B=1,则无法调度此任务
- 到现在为止 , 涉及的 call / jmp 指令:
-
- call 代码段选择子 , 远调用(段间调用)
-
- call 调用门选择子 , 特权级远调用
-
- call TSS选择子/任务门, 调度任务
- 执行任务的指令: call, jmp ,iret
- 可以使用 call / jmp [ TSS选择子, 任务门 ] , iret 来调度任务
-
- 使用call / jmp , 这2个调度任务的指令将 隐含执行的指令:ltr ,lldt
-
- 如果是嵌套的任务,EFLAGS(NT=1), 则可以使用 iret 来调用任务,同样的隐含(ltr,lldt)
- 无论是call/jmp/iret在切换前都会保存当前任务的所有状态(上下文)
-
也就是把当前所有寄存器的值保存到TR寄存器指向的TSS结构
-
以及隐含执行指令(ltr,lldt)
- 如果是:[ call, 中断,异常 ]执行新任务,则新任务的TSS的NT位(被嵌套的任务)=1. 同时把新TSS描述符的B=1
-
只要是call,中断,异常进行切换的新任务,此新任务都称为被嵌套的任务
-
新任务TSS中eflags的NT=1,新任务tss描述符B=1,TSS中的previousLink位被赋值成旧任务的tss选择子
- 如果是:jmp 启动新任务,把当前任务描述符的B位=0,把新任务的B=1
- iret:
- 根据当前任务EFLAGS的NT位来判断
-
- 如果NT=0,则是中断返回
-
- 如果NT=1,则是任务返回:(根据previous位决定返回到哪个任务)
-
- 先把NT=0,B=0,保存当前任务上下文(所有寄存器)到 TR指向的TSS结构
-
- 最后根据TSS的previous位获取tss选择子,并加载,恢复到上一个任务中
-
- 如果当前描述符的B=0,使用了iret ,会有异常
- 总的来说
-
- 想要通过call,jmp来执行任务,那么需满足新任务TSS描述符B=0,否则就是重入
-
- 通过iret来切回上一个任务,需满足当前任务描述符的B=1
-
- 任务在切换前,首先会把当前上下文全部保存到TR指向的TSS结构,然后再加载新任务
- 加载TSS选择子到TR寄存器( ltr ),包括加载TSS结构中的LDT( lldt ),CR3,以及其他一堆寄存器
-
ltr 只是加载并把TSS描述符的B=1,并不会运行此任务
- 接下来从新任务的cs:eip开始执行,特权级根据cs的DPL决定,与旧任务的特权无关
如何让当前的代码与一个TSS结构产生关联
切换任务伪代码
- 假设当前是内核代码,如何让本身成为一个任务 ?
- 伪代码:
;假设当前在内核中
;首先要为内核 分配一个TSS结构,至少104字节
;特权栈不需要,LDT不需要, 所有寄存器也全都不需要赋值
;需要做的就是加载一个空的TSS结构, 让TR指向此TSS结构即可
;当任务切换时, 会把当前内核代码的所有寄存器保存到此TSS结构中
call create_tss ;创建一个TSS结构
call create_descriptor ;为tss创建描述符
call add_to_gdt ;把TSS描述符加入GDT中
;ltr 将修改描述符中的B位
;加载完成后,内核代码就相当于一个任务,虽然这是个空的TSS结构
;目的在于 下面任务切换的时候, 能把内核代码上下文保存在这个TSS结构中
ltr 内核TSS选择子 ;加载这个空空如也的TSS结构, ltr只加载,不运行
- 上述做完后, 为用户也同样分配一个TSS结构,建立描述符:
;在内核代码中为用户分配TSS结构,建立描述符
call create_tss ;创建用户TSS结构
;用户的TSS结构需要初始化,例如 cs,eip,ss ,ds,gs,fs,es, LDT字段, 特权栈
;段寄存器必须做好初始化工作,以防止用户程序错误使用了内核的段寄存器
;当切换任务后, 会把此TSS结构的所有寄存器的值恢复到对应的寄存器中
;所以一个任务第一次运行的时候,必须初始化
call init_user_tss
call create_descriptor ;为tss创建描述符
call add_to_gdt ;把TSS描述符加入GDT中
;接下来需要使用 call / jmp 来执行这个 用户的任务
;此时将把 当前所有的寄存器保存到 tr 指向的内核TSS结构中
;访问并加载TSS描述符到 tr 中
;将用户TSS结构中的eflags中的NT位=1, previousLink=内核tss选择子
;用户TSS描述符B=1.
;这里将隐含的执行 ltr ,lldt 这些指令
;最后,从 用户TSS结构初始化的 cs:eip 开始执行
;注:这里将忽略偏移地址,与调用门类似
call 用户TSS选择子:0x0000 ;从这开始执行用户任务
;用户任务应该有一条 iret 指令
;当用户任务中的 iret 执行时, 查看用户任务TSS结构的NT位,如NT=1,则
;保存用户任务的上下文到TR指向的用户TSS结构中,把用户任务的NT=0,B=0
;从用户TSS结构中的previousLink获取 内核tss选择子, 并执行切换
;加载后, TR=内核tss选择子,从TSS结构中恢复上下文
;cs=当前代码段, eip 指向下面的 hlt 指令
hlt
;如果把上面的 call 改成 jmp
;则将内核TSS描述符B=0,把用户TSS描述符B=1
;既不会填写previousLink,也不会修改用户TSS结构的Eflags.NT
;然后保存内核上下文到TR指向的内核TSS, 加载用户TSS, 开始执行用户代码
;这种情况下,如果用户代码中有iret将出错
;因为iret根据eflags.NT来判断是中断返回,还是任务嵌套返回
;jmp 用户TSS选择子:0x0000
call , jmp 指令作用于任务切换
- call / jmp 0x30:0x0
-
- 如果0x30是:代码段描述符的选择子,则段间远转移
-
- 如果0x30是:调用门描述符的选择子,则特权远转移, 忽略偏移
-
- 如果0x30是:任务门描述符的选择子 或 TSS描述符的选择子, 则任务切换,忽略偏移
call,jmp 指令对任务状态改变
- TSS描述符是系统段描述符(S=0),TYPE=1001(1011表示繁忙)
- 涉及TSS描述符中TYPE的B位(busy)的改变
- EFLAGS中的NT位(NESTED TASK) 嵌套位
- 新任务 TSS结构中的previous link位
- call 指令发起的任务切换,因为还会回到旧任务中:
-
- 旧任务状态不变(B,NT位都不变)
-
- 新任务是被嵌套的(新任务NT=1),previous link=旧任务TSS选择子,B=1
- iret 指令用于任务返回:
-
- 检查当前任务的NT位,如果NT=1则是嵌套的,把当前任务NT=0,B=0,根据previous link位返回到旧任务
-
- 如果NT=0,则是中断返回
- jmp 发起的切换,不是嵌套的:
-
- 旧任务(B=0),其他不变
-
- 新任务(B=1),其他不变
- 处理器通过TSS描述符的B位来检测重入,任务是无法重入的
指令 | 任务-1 | 任务-2 |
---|---|---|
call | B=1,NT=0,previousLink=0.切换后状态不变 | B=1,NT=1,previousLink=任务1的tss选择子 |
jmp | B=1,NT=0,previousLink=0.切换后B=0 | B=1,NT不变,previousLink不变 |
从任务2 iret | B=1,NT=0,previousLink=0 | B=0,NT=0 |
半自动切换任务伪代码
- 假设为每个任务都创建一个 维护控制块 : TCB , TCB中保存着 下一个任务地址,当前任务状态和TSS基址,TSS选择子
- PCB的状态字段,与TSS描述符中的B位 同步
- 这样每一个任务需要: TCB, TSS结构,TSS描述符 3样东西
- PCB 通过链表保存,每增加一个任务就加入链表中
- 提供一个过程switch_task, 循环链表, 查找链表中PCB状态为:0 的PCB,找到后使用jmp指令调度任务
- jmp调度的任务:
-
- 把当前上下文保存到TR指向的TSS结构中
-
- 把当前TSS描述符B=0
-
- 加载新TSS, 把新任务TSS描述符B=1
-
- 开始执行
- TCB:
TCB:
0x00 : 下一个任务地址
0x04 : 当前任务状态 , 0:不忙, 1:忙 (与TSS描述符B位一致)
0x08 : TSS基址
0x0c : TSS选择子
- 伪代码:
section data vstart=0 align=16
tcb_header dd 0 ;链表头
tcb_curr dd 0 ;当前任务
data_end:
section function vstart=0 align=16
;切换任务
switch_task:
push ebp
mov ebp,esp
pushad
push es
push ds
;循环链表,找到下一个TCB状态为0的任务
;设置旧TCB状态为0, 设置新TCB状态为1
;使用jmp TSS选择子,进行任务切换
mov eax,SEL_4G_DATA ;4g段选择子
mov es,eax
mov eax,SEL_DATA ;自己数据段
mov ds,eax
;从当前正在运行的TCB往后找
;先判断是不是只有内核一个任务
mov eax,[ds:tcb_header]
mov ecx,[es:eax]
cmp ecx,0
jz .switch_task_done ;只有内核一个任务,就不切换
mov ebx,[ds:tcb_curr] ;从当前运行的任务后面开始找
.find_next_ready_task:
mov ecx,[es:ebx] ;首4个字节是下一个TCB地址
or ecx,ecx ;下一个为空
jz .find_from_begining ;从头开始
cmp dword [es:ecx+0x04],0
jz .run_task ;找到了
mov ebx,ecx ;找下一个
jmp .find_next_ready_task
;从头开始找
.find_from_begining:
mov ebx,[ds:tcb_header]
cmp dword [es:ebx + 0x04],0
cmovz ecx,ebx
jnz .find_next_ready_task
;开始运行新任务
.run_task:
mov ebx,[ds:tcb_curr] ;修改旧任务状态
mov dword [es:ebx + 0x04],0
mov [ds:tcb_curr],ecx ;修改新任务地址
mov dword [es:ecx + 0x04],1 ;修改新任务状态
jmp far [es:ecx + 0x0c ] ;切换任务
.switch_task_done:
pop ds
pop es
popad
mov esp,ebp
pop ebp
retf
function_end: