posts - 95,comments - 0,views - 12768

任务的隔离

任务

程序时记录在载体上的指令和数据,其正在执行中的一个副本就叫做任务

简单来说,我们为了完成某项特定的工作,写了一个程序,然后这个程序有可以分成几个小程序,程序执行时,每个小程序也在内存中运行,那这个小程序就是一个任务了

任务的LDT

LDT 叫做 局部描述符表,之所以叫局部是因为它是独属于某个任务的,换句话说,LDT中存放的应该是某个任务的私有内存段描述符,而且 LDT 的0号槽位也是可以用的,不想 GDT 那样0号槽位必须是空描述符

任务的TSS

TSS 叫做 任务状态段,这是 每个任务用来保存相关信息的额外使用的额外内存段,最小尺寸是 104字节,TSS中的每个元素能被处理器固件识别,这主要是为了实现任务切换

以下是TSS的示意图,知道它大概记录了啥就行了

1

多任务系统

以下是多任务系统的组成示意图
2

我们看到处理器内部:

  • 处理器使用 GDTR寄存器 追踪GDT,GDT是 全局性 的,为所有任务服务,即是 所有任务共有 的,所以只有1个
  • 处理器使用 LDTR寄存器 追踪LDT,LDT是 局部性 的,为单个任务服务,是 任务私有 的,所以LDT的数量不止一个,视任务多少而定。LDTR 指向的是当前执行的任务
  • 处理器使用 TR寄存器 追踪TSS,TSS也是单个任务私有的,记录当前任务的状态,TR 指向的是当前执行的任务的状态段

有了这些概念后,我们再来简单的谈谈任务切换:

  • 保护当前任务的运行状态,这个就是靠TSS实现啦,主要是一些寄存器的状态
  • 切换LDTR和TR,让他们指向新的任务
  • 恢复旧任务的运行状态,其实就是切换LDTR和TR,让他们指向旧的任务

全局空间和局部空间

单任务

2

每个任务实际包括两个部分:全局部分和私有部分

任务是在内存中运行的,所以全局/私有部分本质上还是 地址空间的划分------全局/局部(地址)空间

地址空间的访问是依靠 分段机制 进行的,也就是通过描述符表中的描述符来访问内存段,故而:

  • 全局地址空间由GDT定义
  • 局部地址空间由LDT定义

理清了这些概念后,全局/局部空间中的内容是什么呢:

  • 全局空间:含有操作系统的软件和库程序,以及可以调用的系统服务和数据
  • 局部空间:每个任务各自的数据和代码,与要解决的问题有关,各不相同

现在让我们粗浅的来看一下一个任务是如何执行的:
通常,任务会在自己的局部空间运行,当它需要操作系统提供的服务时,转入全局空间执行

多任务

3

可以看到多个任务就有多个局部空间,然后他们都共享一个全局空间

特权级保护概述

试着想象一下,我有一个任务A,它很大胆的修改了全局空间中的部分内容并且成功了,当任务B运行在全局空间时,它实际上运行的不是它认知中的内容了,而是任务A修改后的内容

这个例子是为了说明,虽然我们引进了LDT和TSS,进一步强化了分段机制,将任务与任务之间隔离从而实现多任务,但它还是不够安全,所以我们引入了 特权级保护机制,特权级就是通过级别 限制段的访问和修改,简单来说就是让处理器知道哪个段才是老大

4

上图是Intel处理器提供的4级环状保护结构,数值越小,级别越高,可靠性越高

DPL

DPL叫做 描述符特权级,是描述符中的两比特的字段,用于表示描述符所指向段的特权级

两比特可以取值00,01,10,11,分别对应特权级0,1,2,3

对于数据段 来说,DPL决定了访问该段所应当具备的最低特权级别

CPL

当处理器正在一个 代码段 中取指令和执行指令时,该段的特权级叫做 当前特权级,即CPL;正在执行的这个代码段的选择子位于CS寄存器中,其 最低两位 就是当前特权级的数值

操作系统代码执行时CPL是0,这 能够保证它的安全性,并使它能够访问所有软硬件资源

用户程序在局部空间中执行时,CPL是3,调用系统服务,进入操作系统内核,在全局空间中执行时,CPL是0。所以说,任务的特权级别并不是固定的,要具体看待(CPL取决于CS寄存器)

特权指令

只有在当前特权级CPL为0时才能执行的指令,叫做 特权指令

例如:

  • 加载GDT的 lgdt 指令
  • 加载LDT的 lldt 指令
  • 加载TSS的 ltr 指令

IOPL

IOPL 叫做 输入/输出特权级,代表当前任务的I/O特权级别

处理器允许对各个特权级别所能执行的I/O操作进行控制,通常是 端口访问的许可权(设备的访问通过端口进行)

所以我们可以简单将 IOPL 理解为 访问外设的能力

IOPL位 位于 标志寄存器EFLAGS中
4

RPL

RPL 叫做 请求特权级

不管是实施控制转移,还是访问数据段,都可以看成是一个请求---请求者提供一个段选择子,请求访问指定的段

所以,我们可以 将RPL看成是请求者(提供选择子)的特权级别

这里要先了解一些特权级检查的知识,可以先移步下一小节

大多时候 RPL=CPL,当有时候,经过特权级转移,比如使用调用门,会使得 CPL 发生改变,从而让 RPL不等于CPL

为什么要引入RPL呢:

考虑以下情况,假设应用程序知道了操作系统数据段的选择子,并希望通过该选择子访问修改。低级访问高级,我们很容易就知道不可能,但这实际上是有可能的(如果没有RPL)
5

应用程序想要从硬盘读取数据并写入操作系统的数据段中,应用程序的CPL为3,硬件访问权限不够,所以通过调用门调用操作系统提供的例程,此时CPL为0,然后操作系统根据提供的目标数据段选择子,检查CPL=0,和目标数据段的DPL=0一样,可以写入,于是就将数据写入了

应用程序将数据写入到了操作系统的数据段中,这无疑是不能忍受的,所以就引入了RPL,这是为了搞清楚真正的请求者是谁,如果一开始发现特权级位3的请求者想要访问特权级位0的数据段,那可以直接引发异常中断,不就安全了

段描述符用于描述内存段,们描述符用于描述可执行的代码,比如一段程序、一个例程或一个任务

门的类型

  • 调用门:不同特权级之间的过程调用
  • 中断门/陷阱门:作为中断处理过程使用的
  • 任务门:对应着单个任务,用来执行任务切换

调用门

6

以上是调用门描述符的格式

以下是对各个部分的解析:

  • 调用门描述符包含了 目标代码段的选择子,这方便了对代码段描述符 有效性、段界限和特权级的检查
  • 目标例程在代码段内的 偏移量 是直接指定的,所以当我们通过调用门选择子跳转时,在指令中给出的偏移量会被忽略
  • TYPE字段用于标识门的类型,1100 表示调用门

7
调用门之所以叫做门不是随便取的,我们可以看到上图,这其实就是使用调用门的检查,只有在 门内 的特权级才能够使用这个门

具体来说只有满足以下两个条件才能使用调用门:即 数值 上要求

  1. CPL<=调用门描述符的DPL
    RPL<=调用门描述符的DPL
  2. CPL>=目标代码段描述符的DPL

有了这个理性的认识,在结合上图,我们就能够知道,调用门的DPL是特权级检查的下限,目标代码段的DPL是特权级检查的上限,一个上限一个下限,是不是很像一个门呢

特权级检查

特权级的检查发生在段选择子传送到段寄存器时,代码段的检查是CS,数据段的检查是DS、ES、FS、GS,栈段的检查是SS

代码段的特权级检查

  • 一般来说,控制转移只允许发生在 两个特权级相同的代码段之间,即

    • CPL=目标代码段的DPL
    • RPL=目标代码段的DPL
  • 低特权级转移到高特权级:主要是为了让 特权级低的用户程序可以调用特权级高的操作系统例程

    • 将高特权级代码段定义为 依从的 : 两个条件

      • 代码段描述符的 TYPE字段有C位 ,C=0表示代码段只能供同特权级的程序使用,C=1表示代码段为依从代码段
      • 当前特权级CPL和RPL必须低于或等于目标代码段描述符的DPL,即 数值 上要求
        • CPL>=目标代码段的DPL
        • RPL>=目标代码段的DPL

      需要注意的是,依从的代码段是在调用程序的特权级运行,简单来说就是当控制转移时,CPL不变,被调用过程的特权级依从于调用者的特权级

    • 使用门(门描述符):特指调用门

      • jmp far 调用门的选择子: 将控制通过门转移到比当前特权级高的代码段,但 不改变当前特权级别
      • call far 调用门的选择子: 当前特权级会提升到目标代码段的特权级别
  • 高特权级到低特权级:
    除了从高特权级别的例程返回外,不允许从特权级高的代码段将控制转移到特权级低的代码段,这是因为特权级越高,可靠性越高,操作系统不会引用可靠性比自己低的代码

数据段的特权级检查

  • 高特权级别的程序可以访问低特权级别的数据段, 反之不行,即 数值 上要求
    • CPL<=目标数据段的DPL
    • RPL<=目标数据段的DPL

栈段的特权级检查

  • 任何时候,栈段的特权级别必须和当前特权级CPL相同,即 数值 上要求
    • CPL=目标栈段的DPL
    • RPL=目标栈段的DPL

栈切换

当一个用户程序进行特权级切换后,使用的栈也要进行切换

比如说特权级位3的用户程序通过调用门进入到特权级0,那么对应的栈也要跟着切换,从3特权级栈切换到0特权级栈

总之,栈段的特权级必须同当前特权级保持一致,这是为了 防止因栈空间不足而产生不可预料的问题,同时也是为了防止栈数据的交叉引用

额外定义的栈

为了切换栈,每个任务除了自己固有的栈之外,还必须额外定义几套栈

例如,3特权级需要 额外 定义 0/1/2 特权级的栈,2特权级 额外 需要定义 0/1 特权级的栈......

这些额外的栈是由 操作系统加载程序时自动创建的,描述符位于LDT中,同时还必须在任务的TSS中登记

栈切换是处理器固件根据TSS信息自动完成的

参数复制

调用者通常会将例程所需要的参数压入栈中,调用门描述符有一个参数个数字段用来记录所需参数个数

栈切换时,处理器会自动替换 SS和ESP的内容,使它指向新栈,同时还会将旧栈的参数赋值到新栈中

内核程序的初始化

MBR加载内核程序并初始化GDT

8

调用门的安装

这里的目的就是将内核数据段中的SALT表(存储对外公开的例程)的入口点替换为 特权级为3的调用门选择子 ,而不是之前写入的 公共例程段+段偏移量

然后将调用门描述符安装进 GDT中

用户程序就可以通过例程的名称在C-SALT(内核数据段中的SALT表)找到对应的选择子,然后通过访问GDT/LDT找到对应的描述符,最后就能够使用例程了

   ;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
     mov edi,salt                       ;C-SALT表的起始位置 
     mov ecx,salt_items                 ;C-SALT表的条目数量 
.b3:
     push ecx   
     mov eax,[edi+256]                  ;该条目入口点的32位偏移地址 
     mov bx,[edi+260]                   ;该条目入口点的段选择子 
     mov cx,1_11_0_1100_000_00000B      ;特权级3的调用门(3以上的特权级才
                                        ;允许访问),0个参数(因为用寄存器
                                        ;传递参数,而没有用栈) 
     call sys_routine_seg_sel:make_gate_descriptor
     call sys_routine_seg_sel:set_up_gdt_descriptor
     mov [edi+260],cx                   ;将返回的门描述符选择子回填
     add edi,salt_item_len              ;指向下一个C-SALT条目 
     pop ecx
     loop .b3

安装调用门后的GDT布局
9

任务控制块

任务控制块,即 Task Control Block 简称 TCB

TCB是内核为每一个任务创建的一个内存区域,用来记录任务的信息和状态

你可能会疑惑,不是有TSS了吗?TSS是任务切换时处理器要求的,而TCB是内核创建任务时需要的。两者面向的对象和使用的目的并不相同

如下是TCB的结构:

10

TCB链

内核为了追踪所有任务,也就是所有TCB,使用的是链表这种数据结构,也就是TCB链,我们能够观察到TCB结构的第一个双字存储的是 下一个TCB基地址,这其实就是指针,该字段为0表示后面没有任务了

我们要先创建一个双字大小的 tcb_chain 用来表示TCB链的头部,使它指向第一个任务,如果为0表示没有任务

11

这里会有一个小问题,那就是链首指针是在内核数据段中声明并初始化的,所以只能知道它的偏移量(即让ds指向内核数据段,通过 [ds:tcb_chain] 的方式访问);而链上的每个TCB是动态分配的,只能通过线性地址访问(即让es指向4G内存段,通过 [es:线性地址] 的方式访问)

TCB链上追加TCB的流程

12

访问栈中过程参数

之所以叫过程参数,是因为我们调用例程的时候,通常是将参数压入栈中存储,而不是通过寄存器传递

隐式访问

就是通过push、pop、call、ret等指令,这需要用到ESP寄存器,比较固定,要遵守先进后出的机制

显示访问

这种方式是将栈看成一般的数据段,直接访问其中的任何内容,这要用到栈基址寄存器EBP,使用EBP进行寻址时,默认使用段寄存器SS,不需要加上段超越前缀

12

如上图右侧所示,我们可以使用EBP访问栈中的任何地方的数据,至于为什么不使用ESP,可能是因为,ESP必须指向栈顶元素吧,显示的使用ESP可能不安全

pushad指令

pushad指令是压入8个双字,其实就是8个通用寄存器的内容,分别是EAX/EBX/ECX/EDX/ESP/EBP/EDI/ESI 这8个

popad指令

这就是与pushad对应的弹出指令了

加载用户程序并创建任务

当用户程序被读入内存,并处于运行或者等待运行的状态时,就视为一个任务,任务通过描述符来引用自己的代码段和数据段(包括栈)

准备工作

  • 分配一块内存作为LDT
  • 分配内存并加载用户程序
  • TCB中登记
    • LDT的界限和基地址
      • 需要注意的是,初始的时候,LDT的总字节数是0,那么界限就是总字节数减1,也就是 0xFFFF
    • 用户程序加载基地址

创建LDT(用户程序的段描述符)

  • 建立头部段描述符,并将其安装到LDT中,并登记 头部选择子TCB 和回填到 头部内
  • 建立代码段描述符,并将其安装到LDT中,并回填 代码段选择子头部内
  • 建立数据段描述符,并将其安装到LDT中,并回填 数据段选择子头部内
  • 建立堆栈段描述符,并将其安装到LDT中,并回填 堆栈段选择子头部内

重定位U-SALT表(用户程序的SALT表)

和之前将SALT表中的例程名字符串替换为 段选择子和段内偏移 一样,只不过这里是将其替换为 调用门选择子和段内偏移,需要注意的是调用门选择子的RPL字段应该为3

创建0/1/2特权级的栈

  • 分配内存作为栈空间
  • 创建栈描述符---栈段、4KB粒度、DPL
  • 安装到LDT中
  • TCB登记
    • 以4KB为单位的堆栈长度
    • 堆栈使用高端地址作为基地址
    • 堆栈选择子
    • 堆栈初始ESP(一般为0)
   mov esi,[ebp+11*4]                 ;从堆栈中取得TCB的基地址

   ;创建0特权级堆栈
   mov ecx,4096
   mov eax,ecx                        ;为生成堆栈高端地址做准备 
   mov [es:esi+0x1a],ecx
   shr dword [es:esi+0x1a],12         ;登记0特权级堆栈尺寸到TCB 
   call sys_routine_seg_sel:allocate_memory
   add eax,ecx                        ;堆栈必须使用高端地址为基地址
   mov [es:esi+0x1e],eax              ;登记0特权级堆栈基地址到TCB 
   mov ebx,0xffffe                    ;段长度(界限)
   mov ecx,0x00c09600                 ;4KB粒度,读写,特权级0
   call sys_routine_seg_sel:make_seg_descriptor
   mov ebx,esi                        ;TCB的基地址
   call fill_descriptor_in_ldt
   or cx,0000_0000_0000_0000          ;设置选择子的特权级为0
   mov [es:esi+0x22],cx               ;登记0特权级堆栈选择子到TCB
   mov dword [es:esi+0x24],0          ;登记0特权级堆栈初始ESP到TCB

安装LDT描述符到GDT中

13

以上是LDT描述符的结构,基本上和GDT的一样

S位为0表示是系统的段描述符或者门描述符,S=1表示是普通的段描述符

TYPE字段为 0010 表示这是一个LDT描述符

  • 安装LDT描述符到GDT中
  • TCB登记LDT选择子
   ;在GDT中登记LDT描述符
   mov eax,[es:esi+0x0c]              ;LDT的起始线性地址
   movzx ebx,word [es:esi+0x0a]       ;LDT段界限
   mov ecx,0x00408200                 ;LDT描述符,特权级0
   call sys_routine_seg_sel:make_seg_descriptor
   call sys_routine_seg_sel:set_up_gdt_descriptor
   mov [es:esi+0x10],cx               ;登记LDT选择子到TCB中

任务状态段TSS的格式

大多数看TSS的结构图就能知道是什么意思,这里说几个比较模糊的

  • CR3和分页机制有关,没有使用分页,可以为0

  • T位用于软件调试

  • I/O映射基地址用于决定当前任务是否可以访问特定的硬件端口

I/O许可位串

我们知道标志寄存器中的IOPL位决定了当前任务的I/O特权级别,当CPL级别不低于IOPL时,所有I/O操作都允许,当CPL级别低于IOPL时,处理器规定大部分不允许,哪个端口允许就要 找到当前任务的TSS,并检索I/O许可串,这也就是I/O映射基地址所指向的地方啦

I/O许可串是一个 比特序列,最多雨荨65536比特,也就是8KB,第1个比特表示0号端口,第2个比特表示1号端口......

并且0表示允许访问,1表示禁止访问

15

I/O映射基地址位于TSS内偏移为102的 字单元,保存着I/O许可串(I/O许可位映射区)的起始地址,如下图所示

16

TSS的界限值包括了I/O许可位映射区在内,如果TSS中的I/O映射基地址所在的字单元的内容大于或者等于TSS的段界限,就表明没有I/O许可位串

I/O端口按字节编址

I/O端口按字节编址的意思是 每个端口仅被设计用来读写一个字节的数据,当以字或双字访问时,实际上是访问连续的2个或4个端口,这也意味着 会检查I/O许可位串的连续2个或4个位

所以这就可能导致越界问题,当我们按字访问最后一个字节时,两字节的读操作会越界,所以处理器要求I/O许可位映射区最后额外加了一个字节 0xFF(位于TSS界限内)

创建TSS并安装到GDT中

  • 创建TSS

    • 分配内存作为TSS
    • TCB登记TSS的基地址和界限值(界限值必须至少是103字节)
       ;创建用户程序的TSS
       mov ecx,104                        ;tss的基本尺寸
       mov [es:esi+0x12],cx              
       dec word [es:esi+0x12]             ;登记TSS界限值到TCB 
       call sys_routine_seg_sel:allocate_memory
       mov [es:esi+0x14],ecx              ;登记TSS基地址到TCB
    
    • TSS登记表格内容(暂存在TCB中)
      • 指向前一个任务的指针填写为0,表示是唯一的任务
      • 0/1/2特权级堆栈的选择子和初始ESP
      • 当前任务的LDT选择子
      • 填写I/O许可位映射区的地址
      ;登记基本的TSS表格内容
      mov word [es:ecx+0],0              ;反向链=0
    
      mov edx,[es:esi+0x24]              ;登记0特权级堆栈初始ESP
      mov [es:ecx+4],edx                 ;到TSS中
    
      mov dx,[es:esi+0x22]               ;登记0特权级堆栈段选择子
      mov [es:ecx+8],dx                  ;到TSS中
    
      mov edx,[es:esi+0x32]              ;登记1特权级堆栈初始ESP
      mov [es:ecx+12],edx                ;到TSS中
    
      mov dx,[es:esi+0x30]               ;登记1特权级堆栈段选择子
      mov [es:ecx+16],dx                 ;到TSS中
    
      mov edx,[es:esi+0x40]              ;登记2特权级堆栈初始ESP
      mov [es:ecx+20],edx                ;到TSS中
    
      mov dx,[es:esi+0x3e]               ;登记2特权级堆栈段选择子
      mov [es:ecx+24],dx                 ;到TSS中
    
      mov dx,[es:esi+0x10]               ;登记任务的LDT选择子
      mov [es:ecx+96],dx                 ;到TSS中
    
      mov dx,[es:esi+0x12]               ;登记任务的I/O位图偏移
      mov [es:ecx+102],dx                ;到TSS中 
    
      mov word [es:ecx+100],0            ;T=0
    
  • 安装TSS描述符到GDT中

      ;在GDT中登记TSS描述符
      mov eax,[es:esi+0x14]              ;TSS的起始线性地址
      movzx ebx,word [es:esi+0x12]       ;段长度(界限)
      mov ecx,0x00408900                 ;TSS描述符,特权级0
      call sys_routine_seg_sel:make_seg_descriptor
      call sys_routine_seg_sel:set_up_gdt_descriptor
      mov [es:esi+0x18],cx               ;登记TSS选择子到TCB
    

以下是TSS描述符的格式,
14

TYPE字段中的B位表示忙位,任务刚创建时为0,表示任务不忙;当任务开始执行时或者处于挂起状态,由处理器置1

TSS的DPL和RPL均为0

带参数的返回指令

在过程返回之后,需要将调用过程前压入的参数弹出,这时候,我们可以使用带参数的返回指令可以一步达成目的

ret/retf 16位立即数 表示返回到调用者前,从栈中弹出多少字节的数据

用户程序的执行

  • 加载用户程序并创建任务,这一步主要使用到例程 load_relocate_program,代码如下
   ;创建任务控制块。这不是处理器的要求,而是我们自己为了方便而设立的
   mov ecx,0x46
   call sys_routine_seg_sel:allocate_memory
   call append_to_tcb_link            ;将任务控制块追加到TCB链表 

   push dword 50                      ;用户程序位于逻辑50扇区
   push ecx                           ;压入任务控制块起始线性地址 
 
   call load_relocate_program
  • 让设置生效,主要是任务的TSS和LDT
   ltr [ecx+0x18]                     ;加载任务状态段 
   lldt [ecx+0x10]                    ;加载LDT
  • 将控制权交给用户程序,也就是执行用户程序
   mov eax,mem_0_4_gb_seg_sel
   mov ds,eax

   ;以下假装是从调用门返回。摹仿处理器压入返回参数 
   push dword [0x08]                  ;调用前的堆栈段选择子
   push dword 0                       ;调用前的esp

   push dword [0x14]                  ;调用前的代码段选择子 
   push dword [0x10]                  ;调用前的eip

   retf

总结

15

posted on   Dylaris  阅读(36)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示