手写简易操作系统(八)--特权级以及TSS

2024-03-14 21:04

本文主要是介绍手写简易操作系统(八)--特权级以及TSS,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前情提要

我们在这里梳理一下上面几节讲的内容

首先是计算机开机,BIOS接过第一棒,将第一个扇区MBR的内容导入到内存 0x7c00 的位置。

然后就是MBR中我们自己写的内容,将Loader导入到 0x600 的地址,Loader设置了GDT,打开了保护模式,并且开启了内存分页。最后将内核载入到内存的 0xc0001500 的位置,也就是物理内存 0x1500 的位置。

这一节没有代码,我们讲一下特权级的问题。

一、特权级

特权级按照权力从大到小分为0、1、2、3级,数字越小,权力越大,然而实际上Linux中只是用了两个特权级,其中0特权级为内核特权级,3特权级为用户特权级。

为何要设立特权级?为了不让用户程序直接操作硬件,用户程序在操作硬件或者一些危险操作时只能通过操作系统去执行。

二、TSS

TSS,即Task State Segment,意为任务状态段,它是处理器在硬件上原生支持多任务的一种实现方式。

其结构如下

image-20240314155554711

TSS是每个任务都有的结构,它用于一个任务的标识,相当于任务的身份证,程序拥有此结构才能运行,这是处理器硬件上用于任务管理的系统结构,处理器能够识别其中每一个字段。

104字节是TSS的最小尺寸,根据需要,后面IO位图的大小不定。目前只关注28字节之下的部分,这里包括了3个栈指针。

任务是由处理器执行的,任务在特权级变换时,本质上是处理器的当前特权级在变换,由一个特权级变成了另外一个特权级。这就开始涉及栈的问题了,处理器固定,处理器在不同特权级下,应该用不同特权级的栈,所以TSS结构中有三个不同的栈指针,分别对应0,1,2特权级。哎????不对啊,不是四个特权级嘛?先别急

特权级在变换时,需要用到不同特权级下的栈,当处理器进入不同的特权级时,它自动在TSS中找同特权级的栈,你懂的,TSS是处理器硬件原生的系统级数据结构,处理器当然知道TSS中哪些字段是目标栈的选择子及偏移量。

特权级转移分为两类

  • 特权级由低到高,通过中断门或者调用门实现
  • 特权级由高到低,通过调用返回指令实现

对于第一种,由于不知道目标特权级对应的栈地址在哪里,所以要提前把目标栈的地址记录在某个地方,当处理器向高特权级转移时再从中取出来加载到SS和ESP中以更新栈。处理器会自动地从TSS中找到对应的高特权级栈地址,这一点不需要写程序赋值。

对于第二种,当中断发生时,处理器会自动将当前特权级别的栈指针(SS:ESP)保存到中断堆栈(中断栈)中,并切换到特权级0(内核态)来执行中断服务程序。这个过程中,处理器会自动在中断堆栈顶部保存被中断前的 SS 和 ESP 的值。

也就意味着没有哪个特权级的程序会进入3特权级,那么用户程序也就不需要保存3特权级的栈了。

三、CPL、DPL、RPL

x86访问内存的机制是“段基址:偏移地址”,无论是实模式,还是保护模式,都要遵循此方式。保护模式下,段寄存器中的不再是段基址,而是段选择子,通过该选择子从GDT或LDT中找到相应的段描述符,从该描述符中获取段的起始地址。

选择子的0~1位就是RPL字段,它就是请求特权级。即RPL

计算机中,谁是访问者,谁要去访问计算机资源,代码!所以,就用代码段寄存器CS中选择子的RPL位表示代码请求别人资源能力的等级,那么CS寄存器中选择子低2位的值不仅称为请求特权级,又称为处理器的当前特权级,也就是说处理器的当前特权级是CS.RPL。即CPL

在段描述符中有一个属性还为该内存标明了特权等级,这就是段描述符中的DPL,即描述符特权级

3.1、处理器当前特权级为什么会变化

当前正在运行的代码所在的代码段的特权级DPL就是处理器的当前特权级,当处理器从一个特权级的代码段转移到另一个特权级的代码段上执行时,由于两个代码段的特权级不一样,处理器当前的特权身份起了变化,这就是当前特权级CPL改变的原因。

其实就是使用了那些能够改变程序执行流的指令,如int、call等,这样就使CS和EIP的值改变,从而使处理器执行到了不同特权级的代码。

不过特权转移并不是随便转移的,那么设置这个特权级的意义就没有了,处理器要检查特权变换的条件。当处理器特权级检查的条件通过后,新代码段的DPL就变成了处理器的CPL,也就是目标代码段描述符的DPL将保存在代码段寄存器CS中的RPL位。

3.2、第一个处理器当前特权级怎么来的

我们的代码中,再打开保护模式前,CS寄存器的值一直为0。在打开了保护模式后,进行了一次远跳程序刷新流水线,其段选择子为SELECTOR_ CODE,这个选择子的特权级为0,也就是说一进入保护模式,我们的当前特权级就是0了。远跳时,由于是从0到0,才能成功。

3.3、受访者是谁

DPL是段描述符所代表的内存区域的“门槛”权限,访问者能否迈过此门槛访问到本描述符所代表的资源,其特权级至少要等于这个门槛,访问者特权能否大于该门槛?这要看受访资源是代码,还是数据。

所以访问者是代码,受访者是资源,门槛是段描述符

3.4、不涉及RPL受访者为数据段

数据段段描述符中type字段中未有X可执行属性

只有访问者的权限大于等于该DPL表示的最低权限才能够继续访问,否则连这个门槛都迈不过去。

3.5、不涉及RPL受访者为代码段

代码段段描述符中type字段中含有X可执行属性

只有访问者的权限等于该DPL表示的最低权限才能够继续访问,即只能平级访问。

对于受访者为代码段一这说法,实际上是指处理器从当前运行的代码段上转移到受访者这个目标代码段上去执行,并不是说把该目标代码段当数据一样访问。为什么呢?

因为高特权级的代码什么都可以干,没必要降低特权级完成一件事,所以高特权级代码不会降到低特权级,低特权级代码又不能访问高特权级,这就导致只能在平级之间转换。

但凡事皆有例外,处理器从中断处理程序中返回到用户态的时候实现了从高特权降到低特权。

中断处理都是在0特权级下进行的,因为中断的发生多半是外部硬件发生了某种状况或发生了某种不可抗力事件而必须要通知CPU导致的,所以,在中断的处理过程中需要具备访问硬件的能力,在大多数情况下只有CPU处于0特权级才能访问硬件,这是因为eflags寄存器中的IOPL位的值通常被设置为0(该位的作用就是限制访问IO端口的最低特权级),并且TSS中不存在 IO位图,有关这部分后面马上会讲到。再者,有些中断处理中需要的指令只能在0特权级下使用,这部分指令称为特权指令,所以中断发生后其处理的过程必须在0特权级下进行。用户进程是在3特权级,在运行用户程序时若发生了中断,CPU会暂停用户程序的执行,随后CPU就会自动由3特权级进入到0特权级,在0特权级下将执行用户程序时的现场环境(也就是著名的概念:上下文)保存起来(这个保存上下文的动作可以由CPU通过TSS完成,这是CPU在硬件上提供的功能,但其效率并不高,所以大多数操作系统都是自己写代码手动保存上下文环境),待中断处理完成后,CPU会恢复用户程序的执行,也就是说会回到3特权级。

3.6、如何实现特权级转移

如果如上面所说,代码段只能平级访问,那初始进入的是哪个特权级,后面就只能在这个特权级了嘛?这一想也不对,要实现特权级转移有两种方式,一种是使用一致性代码段

一致性代码段也称为依从代码段,Conforming,用来实现从低特权级的代码向高特权级的代码转移。一致性代码段是指如果自己是转移后的目标段,自己的特权级(DPL)一定要大于等于转移前的CPL,即数值上CPL≥DPL,也就是一致性代码段的DPL是权限的上限,任何在此权限之下的特权级都可以转到此代码段上执行。代码段可以有一致性和非一致性之分,但所有的数据段总是非一致的,即数据段不允许被比本数据段特权级更低的代码段访问。

但是一般不用这个,linux中使用了中断,除了中断还有3个门,都可以实现特权级的转移。

四、门

门结构是什么呢?就是记录一段程序起始地址的描述符。

任务门

image-20240314164740718

中断门

image-20240314164754266

陷阱门

image-20240314164807809

调用门

image-20240314164819173

除了任务门外,其他三种门都是对应到一段例程,即对应一段函数,而不是像段描述符对应的是一片内存区域。任何程序都属于某个内存段,所以程序确切的地址必须用“代码段选择子+段内偏移量”来描述,可见,门描述符基于段描述符,例程是用段描述符来给出基址的,所以门描述符中要给出代码段的选择子,但光给出基址远远不够,还必须给出例程的偏移量,这就是门描述符中记录的是选择子和偏移量的原因。

任务门描述符可以放在GDT、LDT和IDT(中断描述符表),调用门可以位于GDT、LDT中,中断门和陷阱门仅位于IDT中

任务门、调用门都可以用call和jmp指令直接调用,原因是这两个门描述符都位于描述符表中,要么是GDT,要么是LDT,访问它们同普通的段描述符是一样的。陷阱门和中断门只存在于IDT中,因此不能主动调用,只能由中断信号来触发调用。

4.1、为何提供了四种门

提供了4种门的原因是它们都有各自的应用环境,但它们都用来实现从低特权级的代码段转向高特权级的代码段。

1.调用门call和jmp指令后接调用门选择子为参数,以调用函数例程的形式实现从低特权向高特权转移,可用来实现系统调用。call指令使用调用门可以实现向高特权代码转移,jmp指令使用调用门只能实现向平级代码转移。

2.中断门以int指令主动发中断的形式实现从低特权向高特权转移,Linux系统调用便用此中断门实现。

3.陷阱门以int3指令主动发中断的形式实现从低特权向高特权转移,这一般是编译器在调试时用。

4.任务门任务以任务状态段TSS为单位,用来实现任务切换,它可以借助中断指令发起。当中断发生时,如果对应的中断向量号是任务门,则会发起任务切换。也可以像调用门那样,用call或jmp指令后接任务门的选择子或任务TSS的选择子。

4.2、我们用到了哪个

中断门,我们仿照Linux使用中断门进行设计,任务门切换任务开销过大。调用门实现复杂且需要自己做特权级的转换。陷阱门只用作调试。

五、逻辑上的漏洞

现在看来好像没什么破绽,通过CPL与DPL保证用户程序可以跳到特权级0,且可以返回特权级3,实现了特权级之间的转换。很和谐。但是要有人搞破坏怎么办,不管受访者的DPL是多少,如果特权检查仅仅靠CPL和DPL这两项的话,数值上CPL≤DPL,这个时候处理器可以访问并获得任何资源。

举个例子,调用门A可以帮助用户程序把硬盘某个扇区的数据写入到用户指定的内存缓冲区中,如果指向的缓冲区是内核的内存地址怎么办。这个时候用户程序通过调用门A把权提到了0,直接把读来的硬盘内容覆盖了内核,电脑gg

发生这种事情的主要原因在于受访问者不知道真正请求资源的是谁,在上面的例子中,真正的资源请求者的特权级为3,但是CPU无法知道,只知道CPL为0,那么什么都可以干,干就完了。

请求特权级RPL完美地解决了这个问题,它代表真正请求者的特权级,在上面的例子中就是3

以后在请求某特权级为DPL级别的资源时,参与特权检查的不只是CPL,还要加上RPL,CPL和RPL的特权必须同时大于等于受访者的特权DPL。

用户程序的CPL是不会骗人的,不可能伪造,它起始是由操作系统在加载用户程序时赋予的,记录在段寄存器CS中的低2位,就是RPL的位置,而CS寄存器只能通过call、jmp、ret、int、sysenter等指令修改,即使改的话,用户程序也只能在3级特权下折腾,只要用户进程不请求操作系统服务,它的CPL是不会变的,当它申请了系统服务,如果提交了选择子作为参数,选择子中的RPL也会被操作系统修改为用户进程的CPL。所以,即使用户程序提交了个伪造的选择子也没用,其RPL会被操作系统用其CPL替换,还其“真身”。

除了加载用户程序时,在其他时段的 CPL是由目标代码段的DPL变成的,即切换到新特权代码段后,新代码段的DPL被存储到段寄存器CS中的低2位,就是RPL的位置。其实这再合理不过了,CPU是切换到新的特权级代码段上运行了,身份变了,当然要用新代码段的DPL做CPL。

受访者若为数据,特权级检查会发生在往数据段寄存器中加载段选择子的时候,数据段寄存器包括DS和附加段寄存器ES、FS、GS

举个例子,mov ds,ax时便会触发特权级检查。ax中的值被当作选择子,处理器会拿ax中的低2位,即RPL和CPL分别与ax中选择子所指向的段描述符的DPL做比较,如果满足RPL≤DPL && CPL≤DPL,选择子才能被加载到DS中。

操作系统提供了一致性代码段和门结构来实现从低特权级到高特权级的代码段转移,这会给恶意攻击者用低特权级的程序访问高特权级资源,使攻击者有诸如篡改内核之类的危险操作的机会,因此必须在CPL和DPL的基础上增加条件。

访问者穿越特权屏障是因为操作系统允许通过特定方式实现从低特权级代码段到高特权级代码段的转移,即通过高特权级代码段来间接获得自身无法拥有的权限。由于真正的资源请求者是低特权级代码段,因此需要标识出资源请求者的真实身份,这就是请求特权级(Request Privilege Level,RPL)的作用。

RPL代表了真正资源请求者的特权级,因此在请求特权级为DPL的资源时,需要检查的不仅是CPL,还要加上RPL,CPL和RPL的特权级必须同时大于等于受访者的DPL。

RPL引入的目的是避免低特权级的程序访问高特权级的资源,有了RPL后,访问内存段的特权检查规则如下(不通过调用门):

  • 如果目标为非一致性代码段,要求数值上:CPL=RPL=目标代码段DPL

  • 如果目标为一致性代码段,要求数值上:CPL≤目标代码段DPL && RPL≥目标代码段DPL

  • 如果目标为数据段时,要求数值上:CPL≤目标数据段DPL && RPL ≤ 目标数据段DPL

栈段的特权级检查比较特殊,因为在各个特权级下处理器都有对应的栈,所以往段寄存器中赋予选择子时,要求CPL等于栈段选择子对应的数据段的DPL,即数值上CPL=RPL=用作栈的目标数据段DPL。

六、IO特权级

IO读写特权是由标志寄存器eflags中的IOPL位和TSS中的IO位图决定的,它们用来指定执行IO操作的最小特权级。IO相关的指令只有在当前特权级大于等于IOPL时才能执行,所以它们称为IO敏感指令(I/O Sensitive Instruction),如果当前特权级小于IOPL时执行这些指令会引发处理器异常。这类指令有in、out、cli、sti。

在eflags寄存器中第12~13位便是IOPL(I/O Privilege Level),即IO特权级,它除了限制当前任务进行IO敏感指令的最低特权级外,还用来决定任务是否允许操作所有的IO端口,对,没错,是全部IO端口,IOPL位是打开所有IO端口的开关(用来单独设置端口访问的方式是IO位图,一会儿介绍)。每个任务(内核进程或用户进程)都有自己的eflags寄存器,所以每个任务都有自己的IOPL,它表示当前任务要想执行全部IO指令的最低特权级,也就是处理器最低的CPL,只有任务的当前特权级大于等于IOPL才允许执行全部IO指令,即数值上CPL≤IOPL;

CPL为0时处理器是法力无边的,所以0特权级下处理器是不受IO限制的。

IOPL是所有IO端口的开关,不过,这个开关还留有余地,如果将开关打开,便可以访问全部65536个端口,如果开关被关上,即数值上CPL > IOPL,则可以通过IO位图来设置部分端口的访问权限。也就是说,先在整体上关闭,再从局部上打开。这有点像设置防火墙的规则,先默认为全部禁止访问,想放行哪些端口再单独打开。

65536个端口号,正好占用8KB。

所以TSS结构变成了这个样子

image-20240314193915164

位图的结尾必须是0xFF,但是位图可以满8KB。

第一,处理器允许I/O位图中不映射所有的端口,即I/O位图长度可以不足8KB,但位图的最后一字节必须为0xFF。如果在位图范围外的端口,处理器一律默认禁止访问。这样一来,如果位图最后一字节的0xFF属于全部65536个端口范围之内,字节各位全为1表示禁止访问此字节代表的全部端口,这并没什么过错。

第二,如果该字节已经超过了全部端口的范围,它并不用来映射端口,只是用来作为位图的边界标记,用于跨位图最后一个字节时的“余量字节”。避免越界访问TSS外的内存。

结束语

这一节讲了很多理论的东西,希望大家没有瞌睡,哈哈哈

这篇关于手写简易操作系统(八)--特权级以及TSS的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/809720

相关文章

【Linux进阶】UNIX体系结构分解——操作系统,内核,shell

1.什么是操作系统? 从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kerel),因为它相对较小,而且位于环境的核心。  从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特生。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等

19.手写Spring AOP

1.Spring AOP顶层设计 2.Spring AOP执行流程 下面是代码实现 3.在 application.properties中增加如下自定义配置: #托管的类扫描包路径#scanPackage=com.gupaoedu.vip.demotemplateRoot=layouts#切面表达式expression#pointCut=public .* com.gupaoedu

17.用300行代码手写初体验Spring V1.0版本

1.1.课程目标 1、了解看源码最有效的方式,先猜测后验证,不要一开始就去调试代码。 2、浓缩就是精华,用 300行最简洁的代码 提炼Spring的基本设计思想。 3、掌握Spring框架的基本脉络。 1.2.内容定位 1、 具有1年以上的SpringMVC使用经验。 2、 希望深入了解Spring源码的人群,对 Spring有一个整体的宏观感受。 3、 全程手写实现SpringM

【操作系统】信号Signal超详解|捕捉函数

🔥博客主页: 我要成为C++领域大神🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞👍收藏⭐评论✍️ 本博客致力于知识分享,与更多的人进行学习交流 ​ 如何触发信号 信号是Linux下的经典技术,一般操作系统利用信号杀死违规进程,典型进程干预手段,信号除了杀死进程外也可以挂起进程 kill -l 查看系统支持的信号

操作系统实训复习笔记(1)

目录 Linux vi/vim编辑器(简单) (1)vi/vim基本用法。 (2)vi/vim基础操作。 进程基础操作(简单) (1)fork()函数。 写文件系统函数(中等) ​编辑 (1)C语言读取文件。 (2)C语言写入文件。 1、write()函数。  读文件系统函数(简单) (1)read()函数。 作者本人的操作系统实训复习笔记 Linux

BD错误集锦5——java.nio.file.FileSystemException 客户端没有所需的特权

问题:在运行storm本地模式程序时,java.nio.file.FileSystemException  客户端没有所需的特权   解决方式:以管理员身份运行IDEA即可。

神经网络第四篇:推理处理之手写数字识别

到目前为止,我们已经介绍完了神经网络的基本结构,现在用一个图像识别示例对前面的知识作整体的总结。本专题知识点如下: MNIST数据集图像数据转图像神经网络的推理处理批处理  MNIST数据集          mnist数据图像 MNIST数据集由0到9的数字图像构成。像素取值在0到255之间。每个图像数据都相应地标有“7”、“2”、“1”等数字标签。MNIST数据集中,

Java的简易编译命令

生成jar包 编译生成.class 文件 编译.class文件的命令,其中的参数是输出,原文件路径 javac -sourcepath class.class MyClass.java 如果有包名的需要创建对应包的文件夹,建文件移动对应的包名下在开始包下创建meta-inf文件夹在meta-inf文件下创建manifest.mf文件 生成可运行的jar包 解压 生成了jar包后,进

HarmonyOS NEXT:华为开启全新操作系统时代

在全球科技浪潮的汹涌澎湃中,华为再次以创新者的姿态,引领了一场关于操作系统的革命。HarmonyOS NEXT,这一由华为倾力打造的分布式操作系统,不仅是对现有技术的一次大胆突破,更是对未来智能生活的一次深邃展望。 HarmonyOS NEXT并非简单的迭代升级,而是在华为多年技术积淀的基础上,对操作系统的一次彻底重构。它采用微内核架构,摒弃了传统的宏内核模式,实现了模块化和组件化的设计理念

Linux操作系统段式存储管理、 段页式存储管理

1、段式存储管理 1.1分段 进程的地址空间:按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从0开始编址。内存分配规则:以段为单位进行分配,每个段在内存中占连续空间,但各段之间可以不相邻。 分段系统的逻辑地址结构由段号(段名)和段内地址(段内偏移量)所组成。 1.2段表 每一个程序设置一个段表,放在内存,属于进程的现场信息