4. 权限,特权,任务说明

权限问题:

  • 由于CPL,DPL 无法完整表达权限的问题.
  • 注: RPL仅是一个参与判断权限的参数,别想复杂了,跟函数里多增一个参数用于判断类似
  • 因此加入RPL.此参数由操作系统来保证,CPU仅使用.
    1. cpu执行的代码都将由CS所指向,因此代码段的特权级是特权的本质,数据段的特权只是设置一个门槛
    1. DPL:访问此描述符的权限
    1. RPL: 想以哪种权限去访问, 操作系统来填写, 一个自由的参数, CPU根据此参数去判断
    1. CPL: 当前正在运行的代码段的权限 (也就是代码段描述符里的DPL, 换个了名字.比如有2个代码段,其中一个在运行,正在运行的那个代码段的权限也叫 当前特权级别)
    1. 代码段描述符里的DPL=CPL=CS选择子中的RPL(16位中的低2位). (特例是跳转到一致性代码段,CPL不变,这部分在下面)
    1. 代码段特权级的改变,会连着栈的特权一起切换,代码段里的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. 为什么需要权限, 下面伪代码示例:
  • 权限示例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: 数据段的权限

    1. 根据CPL, RPL , DPL 这3个来检查
    1. 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 , 因此不做检查
    1. 调用过程的CPL,RPL
    1. 转移到此处的目标描述符的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=0CPL不一致,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 ~ 161514 ~ 131211 ~ 87 ~ 54 ~ 0
段内偏移高16位PDPL0TYPE(1100)000参数个数
31 ~ 1615 ~ 0
段选择子段内偏移低16位
  • 调用门需要4项检查:
  • 当前调用者的CPL
  • 调用门选择子RPL (操作系统 自己维护)
  • 调用门描述符DPL
  • 目标代码段描述符的DPL
  • 检查2步:
    1. 首先要满足 本身能够访问调用门:CPL <= 门DPL, RPL <= 门DPL
    1. 下表格:
指令一致性代码段非一致性代码段
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不变,栈不变
  • 对于非一致性代码段:
    1. CALL指令要求: CPL >= 目标代码段DPL , CPL变成目标代码段DPL, 切换栈(除非CPL=DPL,那么栈不变)
    1. JMP 指令: CPL = 目标代码段DPL , CPL不变,栈不变
  • 看上去内容有点多,需要到处判断,一会门检查,一会代码段DPL检查,还要考虑栈切换的问题
  • 实际没那么麻烦,也就2条需要注意的
    1. 一个是有权限访问门 ,也就是本身权限至少要与门相等 数值上: CPL <= 门DPL, RPL <=门DPL
    1. 调用者代码段的权限,要低于代码段或相等,也就是低权限到高权限,那么数值上: CPL>= 目标代码段DPL
  • jmp 不涉及栈的问题, call 会 push cs push eip 因此会改变栈
  • 至于jmp 和 call 的区别不用记, 让CPU产生保护性异常(GP)告诉我们即可,一旦产生异常说明此处jmp 指令有问题了( CPL != DPL)
  • 对于栈的问题,一句话就解决,CPL一变, 栈跟着变 ; CPL不变,栈也不变

调用门的首次检查例子:

门描述符代码段A代码段B代码段C代码段D-
门DPL=3CPL=3CPL=2CPL=1CPL=0代码段A,B,C,D任意+RPL=0~3都可以访问
门DPL=2CPL=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一个栈

  • 切换栈的流程:

    1. 目标代码段(被调用代码段) 会根据自己的DPL.从TSS中选择一个当前特权的栈
    1. 由于执行完后将返回到之前的代码段中, 因此将先临时保存调用者的SS,esp
    1. 把TSS中对应特权的栈给:ss,esp赋值
    1. 把临时保存的SS,ESP, push 到特权栈中
    1. 根据调用门描述符中的参数个数, 把调用者栈中的参数复制到 特权栈; 如有3个参数,则复制12字节(3*4, 以32位调用门为例, 如果是16位调用门则每个参数2个字节)
    1. 把调用者的cs,eip ,push 到特权栈,用于之后返回
    1. 把调用门中的段选择子,加载到cs, eip = 偏移地址. 开始执行
  • 第6步 :

      1. 跟段间调用的流程类似, 只是不在自己的栈中push, 而是在对应的特权栈中push.
      1. 更准确的说,如果是平级的:当前CPL=目标DPL,则push到当前栈中(栈不变), 否则push到特权栈中
调用者的栈被调用者,特权栈(此栈的段选择子在TSS中)
参数3调用者ss
参数2调用者ESP位置
参数1,esp位置参数3
-参数2
-参数1
-调用者cs
-调用者eip

返回问题

  • 同一特权级别的 近返回 (ret ) 仅会检查界限问题, 再次强调同一特权不涉及栈切换
  • 对于特权改变的远返回, 只能返回特权小的代码段
  • 根据特权栈中的cs段选择子的低2位(RPL)来判定:
    1. 如果 当前CPL = 目标cs.RPL 说明是同一级别的段间远返回 (pop eip , pop cs)
    1. 当前CPL < 目标CS.RPL 也就是 当前CPL <目标DPL ,这是特权需要改变的远返回(需要pop eip,pop cs,pop esp, pop ss)
    1. 例如当前CPL=0, 目标(DPL)CS.RPL=3.这符合条件
  • 假设当前是用户程序CPL=3,特权0的被调用过程,有3个参数被传递进来(通过调用门),结尾处: retf 12,返回流程:
    1. 首先特权0栈:pop eip,pop cs
    1. 当前有3个参数,每个参数4个字节,结尾是retf 12. 需要回收特权0栈的参数空间: esp + 12
    1. 参数上面是用户的ss,esp,因此:pop esp,pop ss
    1. 用户esp+12.还原用户栈空间.(特权0栈的参数是从用户栈中复制的)
    1. 此时,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来判断
    1. 如果 ( 栈中cs.rpl = 当前cpl ) 则是段间返回 (retf => pop eip pop cs)
    1. 如果 (栈中cs.rpl > 当前cpl ) 则是特权改变的远返回,这里就是做手脚的地方
    1. 特权远返回对应指令: 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:(局部描述符表)
    1. LDT 用于为每个任务存放各种描述符的地方( 代码段 数据段), 主要是为了能与GDT中其他描述符隔离开,便于管理, 比如A,B,C,D 4个任务, 他们的各种描述符都放在自己的LDT中
    1. 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 指令:
    1. call 代码段选择子 , 远调用(段间调用)
    1. call 调用门选择子 , 特权级远调用
    1. call TSS选择子/任务门, 调度任务
  • 执行任务的指令: call, jmp ,iret
  • 可以使用 call / jmp [ TSS选择子, 任务门 ] , iret 来调度任务
    1. 使用call / jmp , 这2个调度任务的指令将 隐含执行的指令:ltr ,lldt
    1. 如果是嵌套的任务,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位来判断
    1. 如果NT=0,则是中断返回
    1. 如果NT=1,则是任务返回:(根据previous位决定返回到哪个任务)
    1. 先把NT=0,B=0,保存当前任务上下文(所有寄存器)到 TR指向的TSS结构
    1. 最后根据TSS的previous位获取tss选择子,并加载,恢复到上一个任务中
    1. 如果当前描述符的B=0,使用了iret ,会有异常
  • 总的来说
    1. 想要通过call,jmp来执行任务,那么需满足新任务TSS描述符B=0,否则就是重入
    1. 通过iret来切回上一个任务,需满足当前任务描述符的B=1
    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
    1. 如果0x30是:代码段描述符的选择子,则段间远转移
    1. 如果0x30是:调用门描述符的选择子,则特权远转移, 忽略偏移
    1. 如果0x30是:任务门描述符的选择子 或 TSS描述符的选择子, 则任务切换,忽略偏移

call,jmp 指令对任务状态改变

  • TSS描述符是系统段描述符(S=0),TYPE=1001(1011表示繁忙)
  • 涉及TSS描述符中TYPE的B位(busy)的改变
  • EFLAGS中的NT位(NESTED TASK) 嵌套位
  • 新任务 TSS结构中的previous link位
  • call 指令发起的任务切换,因为还会回到旧任务中:
    1. 旧任务状态不变(B,NT位都不变)
    1. 新任务是被嵌套的(新任务NT=1),previous link=旧任务TSS选择子,B=1
  • iret 指令用于任务返回:
    1. 检查当前任务的NT位,如果NT=1则是嵌套的,把当前任务NT=0,B=0,根据previous link位返回到旧任务
    1. 如果NT=0,则是中断返回
  • jmp 发起的切换,不是嵌套的:
    1. 旧任务(B=0),其他不变
    1. 新任务(B=1),其他不变
  • 处理器通过TSS描述符的B位来检测重入,任务是无法重入的
指令任务-1任务-2
callB=1,NT=0,previousLink=0.切换后状态不变B=1,NT=1,previousLink=任务1的tss选择子
jmpB=1,NT=0,previousLink=0.切换后B=0B=1,NT不变,previousLink不变
从任务2 iretB=1,NT=0,previousLink=0B=0,NT=0

半自动切换任务伪代码

  • 假设为每个任务都创建一个 维护控制块 : TCB , TCB中保存着 下一个任务地址,当前任务状态和TSS基址,TSS选择子
  • PCB的状态字段,与TSS描述符中的B位 同步
  • 这样每一个任务需要: TCB, TSS结构,TSS描述符 3样东西
  • PCB 通过链表保存,每增加一个任务就加入链表中
  • 提供一个过程switch_task, 循环链表, 查找链表中PCB状态为:0 的PCB,找到后使用jmp指令调度任务
  • jmp调度的任务:
    1. 把当前上下文保存到TR指向的TSS结构中
    1. 把当前TSS描述符B=0
    1. 加载新TSS, 把新任务TSS描述符B=1
    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:
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值