80386学习(二) 80386特权级保护

一、80386特权级保护介绍

  80386CPU为了给操作系统提供硬件级的可靠保护,提供了特权级保护功能。80386处于保护模式时,会改变CPU的行为方式,其中便包括开启特权级保护。实现良好的特权级保护是需要软硬件相协调的,CPU提供硬件机制的同时也需要与操作系统相配合,共同实现完善的特权级保护功能。

  要较为全面的理解特权级保护的工作原理,需要了解相互关联的各个机制。下面会介绍在80386特权级保护中起到关键作用的:段描述符和描述符表保护模式下的内存访问方式特权级的不同维度特权级校验规则等内容。

二、段描述符和描述符表

  在8086中,为了能够让程序在内存中浮动的加载装配,通过段地址和段内偏移地址共同组成最终的物理地址。而在保护模式下,段机制依然存在,只不过为了支持特权级保护,在一个段能够被访问之前,需要先进行"登记注册"。

  用于登记注册一个段详细信息的数据结构被称为段描述符(Segment Descripter),固定占8个字节,64位。在一个多任务系统中会定义很多不同的段,一个段就对应着一个段描述符,为了统一的进行管理,需要在内存中开辟一段连续的空间集中存放紧密相连的描述符,而这一段连续的内存空间被称为描述符表。(除了段描述符,还有门描述符等其它类型的描述符)

  描述符表有多种类型,如全局描述符表(Global Descripter Table GDT)、局部描述符(Local Descripter Table LDT)、和中断描述符表 (Interrupt Descriptor Table IDT)等

段描述符

  段描述符占8个字节,共64位。其结构如下图所示,上半部分是高32位、下半部分是低32位。

(截图自 x86汇编语言 从实模式到保护模式)

段基地址

  段基地址是一共32位的线性地址。被分为了物理上不连续的两段是因为之前的80286保护模式寻址是24位的,而为了和80286的兼容便只能在之前的段描述符设计上进行扩充。

  在没有开启页机制的情况下,这32位的段基地址就是最终的物理地址。

段界限

  段界限一共20位。被分为两段的原因和段基地址一样,都是为了兼容之前80286的48位段描述符。段界限用于控制段的拓展范围,在访问时,段内偏移超过段界限时会引发错误。

  对于向上拓展的段,如数据段、代码段来说,偏移量从0开始递增,段界限决定了段内偏移量的最大值。对于向下拓展的段,如栈段来说,偏移量从0开始递减,段界限决定了段内偏移量的最小值。

  有了段界限这一限制,在使用访问段时,就能及时发现程序中对内存段访问的越界行为。更重要的是,段界限的限制能够防止程序越界,访问原本不属于该程序,不能被其访问的内存空间。

G位

  G位(Granularity)粒度位,用于解释段界限的含义。

  段描述符中的段界限是20位的,咋看之下一个段的最大空间似乎最大只能是为20位即1MB,这和80386的最大寻址空间4GB可不太匹配。

  20位的段界限是80386的设计者为了兼容20位寻址空间而采取的兼容性设计,而G粒度位便是其中的关键。当G位=0时,段界限以字节为单位,段的最大界限就是(2^20)*1B=1MB;当G位=1时,段界限以4KB为单位,段的最大界限扩展为(2^20)*4KB=4GB,和80386的最大寻址范围相匹配,此时段界限最小也是4KB。

S位

  S位(Descriptor type)类型位,用于标识当前段的类型。

  当S=0时,表示当前段为系统段;当S=1时,表示当前段是一个代码段或数据段(栈段也被视为特殊的数据段)。

DPL字段

  DPL字段(Descriptor Privilege Level)代表当前段描述符的特权级,占两bit位。

  80386共支持4种特权级级别,分别是0,1,2,3。数字越小,特权级越高,0代表最高特权级,3代表最低特权级。

P位

  P位(Segment Present)段存在位,用于标识当前段是否存在于内存中。

  P=0时,代表段在内存中不存在;P=1时,代表段存在于内存之中。通常段描述符所指向的段总是位于内存中的。但有时物理内存空间紧张时,可能只是预先构造了段描述符,而没有分配使用对应的内存空间;或者有的操作系统会在内存紧张时将不常用的段暂时的交互到磁盘中,腾出内存给当前正在执行的程序,以实现段式虚拟内存。

  上述情况下,段描述符的P位就应该被置为0。CPU在对段描述符所指向的段进行访问时,会对P位进行校验。当发现P=0时,CPU会发出一个异常中断,操作系统应该提供对应的中断处理程序用于将对应内存段从磁盘中置换入内存中,同时将对应段描述符的P位置为1。

TYPE字段

  TYPE字段共4位,用于表示段描述符的子类型。

  (截图自 x86汇编语言 从实模式到保护模式)

  对于数据段来说,TYPE的4位分别是X/E/W/A位,而对于代码段来说,TYPE的4位分别是X/C/R/A位。其中X、A位的含义是相同的,而中间的两位由于数据段和代码段的性质不同,被赋予了完全不同的含义。

既然都是内存段,如何判断一个段是数据段还是代码段呢?

  事实上,一个段是数据段还是代码段并不取决于定义时的段描述符,而取决于当前段被用于何种场景。

  如果被加载在CS代码段寄存器中,便被视为代码段;如果被加载在DS、ES、SS等数据段寄存器、栈段寄存器中,则被视为数据段。

TYPE字段各bit位介绍

  X(eXecutable)位标识当前段是否可执行。数据段总是不可执行的,因此X始终为0;代码段总是可执行的,因此X始终为1。

  A(Accessed)位标识当前段是否已被访问。在段描述符初始化时,应该被设置为0。当对应段被访问时,由CPU硬件将A位设置为1。操作系统在设计虚拟内存管理时,通常需要通过某些算法策略决定应该将哪些内存移入磁盘。段描述符中的A位可以帮助操作系统判断某一段时间内,对应段内存是否被访问过,为置换算法提供一定的依据。A位的清零操作也由操作系统来负责。

  E(Expand)位标识数据段的拓展方向。E=0代表数据段是向上,向高地址方向拓展,一般的数据段都是向上拓展的。E=1代表数据段是向下,向低地址方向拓展,这里主要指的就是栈段。因为入栈时,栈顶指针是自减的。

  W(Writeable)位标识数据段是否可写。数据段总是可读的,但是不一定可写 。W=0时,代表当前数据段不可写,如果对当前段有写入指令执行时,会引发CPU异常中断。W=1时,数据段可读可写。

  C(Confirming)位标识代码段是否是特权级依从的。C=0表示当前代码段是非依从的,意味着当前代码段只能被相同特权级的程序调用(或者门调用);C=1表示当前代码段是依从的,意味着当前代码段允许被更低特权级的程序调用。

关于特权级依从和门调用的概念,会在后面进行进一步介绍。

  R(ReadAble)位标识代码段是否是可读的。代码段总是可以执行的,但80386不允许对一个代码段进行写入(如果要对代码段中的内容进行修改,应该改用一个可写的数据段指向对应的内存空间 (别名))。同时,80386还对代码段的可读性做了限制。R=0时,代表该代码段是不可读的,寻址R=0的代码段将会引发处理器异常中断;R=1时,代表该代码段是可读的。这里的不可读,不是指CPU无法读取内存中的指令内容,而是用于限制程序软件的行为,例如在内存寻址中使用段超越前缀"CS:"来寻址访问代码段中的内容。

全局描述符表GDT

  描述符表中最核心的是全局描述符表。从名称中的全局二字可知,全局描述符表在80386CPU运行时是为整个软硬件提供服务的,只能存在一个。在进入保护模式前,需要事先定义好全局描述符表。

  为了让CPU能够在访问段的时候随时定位并读取到全局描述符表中的数据,80386提供了全局描述符表寄存器(Global Descripter Table Register GDTR)。

  GDTR是48位的,高位的32位用于存放全局描述符表的起始线性地址,低位的16位用于存放GDT的界限。对GDTR赋值的汇编命令为:lgdt  m16&32。 

  32位的起始线性地址,最大可寻址4GB,意味着理论上GDT可以被定义在内存的任意位置。由于进入保护模式前需要先定义GDT,因此GDT一般被设置在实模式下可寻址的1MB内的低地址(进入保护模式后也可重新定义GDT)。

  16位的界限值最大为64KB。由于描述符大小为8个字节(8B),因此GDT中可以存放的描述符的上限为2^10^8=8192个,这在多数情况下是绰绰有余的。

局部描述符表LDT

  现代的操作系统是多任务的,那么什么是任务呢?

  程序是保存在存储介质中的指令和数据的结合体,而正在执行程序的一个副本就是任务。一个程序可以有运行在内存中的多个副本,每个副本都是一个任务。支持多任务的操作系统要让并发的多个任务和谐共处,就需要在任务之间通过一些手段令不同任务间彼此隔离。一般情况下,不同任务之间的内存是不互通的,每个任务都有自己的内存空间,一个任务不能随意访问另一个任务独有的内存空间。

  80386CPU的设计者建议为每个任务分配独有的描述符表,这一描述符表被称为局部描述符表LDT。每个任务私有的内存段,其段描述符不放在GDT中,而是放在LDT中。

  CPU通过可以通过GDTR寄存器来找到GDT所在的位置。同样的,80386提供了局部描述符表寄存器(Local Descripter Table Register LDTR)来追踪LDT的位置。

  和GDT不同的是,每个任务都有着自己的LDT。在多任务轮流执行的多任务系统中,正在执行的任务被成为当前任务,而LDTR则指向当前任务的LDT。任务切换时,LDTR中的数据会发生变化,指向新的当前任务。

三、保护模式下的内存访问方式

  前面介绍了段描述符和GDT,下面说明80386CPU在寻址时是如何与段描述符、GDT交互的。

  在8086中,段寄存器存放的是段基址。CPU进行内存寻址时,16位的段基址左移4位与16位的偏移地址相加生成最终的物理地址。而在80386中,寻址方式发生了一定的变化。

  80386中,段寄存器被扩展为了两部分。实模式下,80386段寄存器只有16位的前半部分参与工作,使用方式和8086的16位段寄存器无异,可以兼容的运行8086程序。而在保护模式下,使用段寄存器进行内存寻址的方式发生了变化。此时,前半部分16位装载的不再是段基址,而是一种被称为段选择子的数据结构,保护模式下段寄存器的前半部分被称为段选择器;后半部分用于存储所加载的段描述符相关数据,被称为描述符高速缓冲器。

  此外,80386在8086的段寄存器CS/DS/ES/SS的基础上,还新增了两个数据段寄存器FS和GS,为复杂汇编程序的开发提供了更好的支持。

  (截图自 x86汇编语言 从实模式到保护模式)

 什么是段选择子?

  前面说到,保护模式下段寄存器中装载的不再是段基址,而是段选择子。但CPU从本质上来说依然是通过段基址+偏移地址进行内存寻址的,只不过在中间引入了一层段选择子的抽象,用于实现特权级保护。

  段选择子是一个16位的数据结构,由三部分组成:占高13位的段描述符索引、TI(Table Indicator)描述符表指示器以及RPL(Request Privilege Level)请求特权级。

   (截图自 x86汇编语言 从实模式到保护模式)

描述符索引

  段描述符索引用于在段描述符表中定位对应的段描述符。如果将段描述符表看作一个结构体数组,那么描述符索引就是数组的下标。

  假如段寄存器加载的是描述符表中第3个段描述符所对应的段,那当前段描述符索引的值就是0000000000010(下标从0开始)。段描述符表所能容纳的最大描述符个数是8192个,13位的描述符索引恰好能够与之一一对应。

描述符表指示器TI

   TI用于标识当前段描述符位于何种描述符表中。TI=0时,表示当前段位于GDT中;TI=1时,表示当前段位于LDT中。根据TI的不同,在加载段选择子时,CPU将会去访问对应的段描述符表,根据描述符索引获取对应的段描述符信息,加载到段寄存器中。

  由于段选择子中并没有段基址、段界限等内存寻址时的关键数据,这些数据都只在段选择子指向的段描述符中。但每次使用段寄存器寻址时,不能总是通过段描述符表获取段描述符数据,频繁的内存寻址效率太低。

  因此,80386的设计者在段寄存器中设置了描述符高速缓冲器。只有当段选择子变化时,段寄存器才需要访问一次描述符表,获取对应的段描述符数据,将其存入描述符高速缓冲器中。之后,对于当前同一段选择子的访问,便可以直接从描述符高速缓冲器中获取数据,极大的提高了CPU通过段寄存器进行内存寻址的性能。

  描述符高速缓冲器和存储器高速缓存一样,是纯硬件控制的,无法通过程序直接访问、修改其中的数据。

请求特权级RPL

  RPL请求特权级,标识着提供段选择子的程序的特权级别。RPL的作用很难单独拎出来说明,会在接下来的段特权级访问保护机制中进行介绍。

GDT和LDT的关系

  为了更好的保护每个任务的LDT,防止其被其它任务随意访问,需要将每个任务的LDT视为一个需要进行特权级保护的段,将每个任务的LDT都注册到GDT中。

  每个任务的LDT,都有一个在GDT中的段描述符与之对应(在段描述符中S位=1的系统段)。80386CPU的LDTR被设计为16位,其中存放的是对应LDT的段选择子,当任务切换时,只需切换LDTR中的段选择子即可。

  通过段选择子寻找对应段时,如果段选择子中的TI=0,代表所要寻找的段描述符在GDT中。CPU根据GDTR中的数据,找到GDT,并按照下标计算偏移量,获取对应的段描述符。如果段选择子中的TI=1,代表所要寻找的段描述符在LDT中,CPU先根据当前LDTR中的段选择子去GDT中寻找对应的LDT段描述符,从中获取当前LDT的线性基地址。接着,再通过所请求的段选择子中的描述符索引在LDT中查找最终所需的段描述符

四、特权级的三个维度

  前面介绍了许多用于实现特权级保护的机制,现在终于可以开始说明80386究竟是如何利用这些机制来完成特权级保护的。

  特权级保护从本质上来说,是保护高特权级的内存、外设等资源不会被没有权限(低权限)的程序访问。主体结构是程序访问资源,而CPU需要在这个过程中进行特权级的校验。

  这里引入三种不同概念的特权级:当前特权级CPL、描述符特权级DPL、请求特权级RPL

CPL当前特权级(Current Privilege Level)

  CPL当前特权级,用于表示当前所运行程序的特权级。更进一步的说,也就是当前CS代码段寄存器中所装载段选择子的后两位所决定的特权级。

  BIOS在加载操作系统并进入保护模式时,处理器会在执行第一条指令时自动的将CPL设置为0,可以看做操作系统在进入保护模式时拥有的最高CPL是从处理器继承而来。之后便由操作系统程序负责整个计算机系统的管理,例如加载用户应用程序时,将用户程序的CPL设置为最低特权级3。应用程序虽然不希望自己被放在最低特权级,但操作系统主导了应用程序的加载,其所使用的段描述符、LDT等都由操作系统创建和管理,应用程序只能专注于自己的业务功能,无权控制自己的CPL当前特权级。

  特权级分为4种,可以被看做几个不同大小的同心圆,像一个个的指环,特权级0也被称为ring0。因此运行在核心处ring0特权级的操作系统程序,也被称为内核(Kernel)程序。

  (截图自 x86汇编语言 从实模式到保护模式)

特权指令

  80386CPU提供了一系列的机制实现特权级保护,如段描述符、GDT等等。全局描述符表GDT是特权级保护机制中的一个关键要素,通过指令lgdt可以进行GDT的设置。可如果本应该被高权限的操作系统管理起来的低特权级程序(CPL=3)也能执行lgdt指令的话,整个特权级保护机制就像一幢宏伟的高楼被抽离了地基,变得脆弱不堪。

  80386的设计者自然不会设计出这种百密一疏的方案。因此,在整个80386的指令集中,其中很多底层的、权限很大的指令被规定只能被最高权限的程序(例如操作系统)执行。让CPU停机也是通过指令来完成的,如果应用程序也能随意的执行停机指令,那将是非常恐怖的事情。

  只有处于最高当前特权级CPL=0的程序才有权限执行的指令,叫做特权指令。其中主要包括停机指令;加载GDT、LDT的指令;读写控制寄存器的mov指令等等。

DPL描述符特权级(Descriptor Privilege Level)

  DPL目标特权级,用于标识所指向目标的特权级。前面提到过,每个段描述符都有DPL字段属性。DPL特权级的高低,决定了能够被位于何种特权级的程序所访问。

  CPU对内存段访问的特权级保护是在段选择子加载的时进行的。当有新的段选择子准备加载到段寄存器时,CPU会根据段寄存器的类型进行相应的校验。

代码段访问的保护机制

  对于代码段寄存器的来说,加载新的段选择子可能意味着CPL的变化,校验比较严格,段间控制转移一般只允许发生在相同特权级的程序之间。也就是说,一个当前特权级CPL为2的程序,只能跳转到另一个特权级DPL同样为2的代码段执行,而无法跳转到特权级DPL为0、1、3的代码段。一般程序内部相同特权级代码段间互相跳转都是没问题的,但还存在一些场景需要允许低特权级的程序去调用高特权级的代码:例如低CPL的应用程序去调用高DPL的系统调用例程。

有两种方法允许低CPL的程序跳转高DPL的代码段:

  一是将高特权级的目标代码段定义为依从的,也就是将代码段描述符中TYPE字段的C位设置为1,代表当前代码段是特权级依从的。当低特权级的程序跳转至高特权级的代码段时,CS的后两位不发生变化,CPL和调用程序保持一致。

  二是通过门来进行,门(Gate)也是一种描述符,被称为门描述符。门描述符区别于段描述符,段描述符描述的是一个段,而门描述符描述的是一段可执行的代码、一个程序或者一个任务,系统调用通常使用门描述符来实现。使用jmp far指令可以将控制通过门调用转移到高特权级代码段,但是依然不改变当前特权级CPL;使用call far指令则在将控制转移到高特权级代码段的同时,还会将当前特权级CPL提升到和目标代码段DPL一致,也就是说,一个CPL为3的应用程序,通过门调用调用到了一个DPL=0的代码段程序,则CPU将会将当前特权级CPL提升为0,和目标代码段特权级保持一致。

数据段访问的保护机制

  数据段访问的保护机制相对来说简单一些:处于低CPL的程序无法访问高DPL的数据段。换句话说,在访问数据段前,向数据段寄存器(DS/ES/FS/GS)载入新的段选择子时,要求当前CPL必须高于或等于目标段DPL(数值上CPL <= DPL)。

  举个例子,在古代等级森严的封建制度下,皇帝可以认为是位于CPL=0的级别,普天之下莫非王土,皇帝可以在自己的国家访问任何它想要访问的领地(数据段)。但反过来位于CPL=3的平民是没法去直接访问皇宫的(DPL=0的数据段)。位于中间级别的CPL=1、2的程序就像地方诸侯,CPL=1的诸侯虽然也无法直接访问皇宫,但对于自己的寝宫(DPL=1的数据段)和平民的家(DPL=3的数据段),都是有权利访问的。

  特别的,为了避免高特权级的程序由于栈空间不足而崩溃以及阻止不同特权级间栈数据的交叉引用,处理器在特权级变化的时候,堆栈也会跟着发生变化。所以,向栈段寄存器SS载入新的段选择子时,要求当前CPL必须完全等于目标的DPL(在数值上CPL = DPL)

  到了这里,看起来80386的内存保护机制似乎已经很完善了。高特权级ring0的操作系统和低特权级ring3的应用程序彼此之间被特权级保护机制隔离开了,低特权级的应用程序不能随意的访问高特权级的操作系统内存;没有提供对应的门描述符或者依从代码段,应用程序也无法调用高特权级的程序。

  那么80386的设计者提供的请求特权级RPL作用又是什么呢?

RPL请求特权级(Request Privilege Level) 

  RPL请求特权级,代表请求者的特权级。在执行段间控制跳转指令时,需要提供目标代码段的选择子,载入CS代码段寄存器;在访问数据段时,也需要将数据段选择子装载入DS、ES等数据段寄存器中。无论是执行控制转移,还是访问数据段,都可以看作是当前执行任务的一个请求,RPL也就是当前请求者的特权级。

  大多数情况下,请求者就是当前任务,因此CPL=RPL。谁负责提供段选择子,谁就是请求者。但在某些时刻,提供段选择子的请求者和当前任务并不相同。

  我们知道使用call far转移指令调用操作系统提供的调用门执行系统调用时,会将CPL从应用程序的ring3提高到操作系统所处的ring0。

  假如操作系统提供了一个系统调用,用于从磁盘中读取数据,并将其写入到应用程序数据段的指定位置中(由于系统调用中可能会执行一些特权指令,或是外设被限制了访问特权级,所以通过调用门call far时会提升CPL)。这个系统调用有三个参数:磁盘的扇区号(指定从磁盘的什么位置读取),需要写入的数据段的段选择子(指定写入哪一数据段),最后一个是数据段的段内偏移地址(用于更精确的控制写入数据的段内位置)。

  这个系统调用的设计看起来还不错,能读取指定磁盘扇区的数据并写入指定数据段中,但却隐藏了一个严重问题

  如果应用程序的编写者是一名恶意的攻击者,他给出的数据段选择子参数指向的不是应用程序自己的数据段,而是操作系统的数据段选择子。虽然只有CPL=0的程序才有权限访问操作系统设置的DPL=0的内核数据段,但是由于通过call far调用门进行系统调用时,会将CPL提升为0,因此这个操作会被允许执行。这是一个很严重的漏洞,通过call far调用门实现的系统调用,模糊了CPL和事实上的请求者特权级的关系,使得只有CPL、DPL的校验机制在这种情况下显得无能为力。CPU很难区分出在段选择子的加载时,这个段选择子究竟是操作系统提供的还是恶意应用程序提供的。

  因此,80386的设计者在CPL、DPL的基础上又提供了RPL请求特权级来解决这个问题。虽然CPU不知道段选择子的提供者是谁,但操作系统是知道的。操作系统在内核中访问内存段时,请求者自然是操作系统自己;而操作系统提供系统调用为应用程序服务时,也能明确知道请求者是低特权级的应用程序。

  在上述磁盘读取系统调用的例子中,操作系统可以在系统调用的程序中修改应用程序提供的段选择子的RPL,将其设置为和应用程序匹配的低特权级后再送入段寄存器中。CPU在校验时,除了要求CPL高于或等于目标段的DPL(数值上CPL <= 目标DPL),也要求给出的段选择子中的RPL也必须高于或等于目标段的DPL(数值上RPL <= 目标DPL)。

  引入了RPL后,并且操作系统在系统调用中合理的设置了段选择子的RPL,上述漏洞就不复存在了。正常的应用程序能够访问磁盘数据,并正确的写入自己的数据段中(CPL=0 RPL=3,目标DPL=3 校验通过);但恶意的应用程序即使传入的段选择子RPL=0,也会被系统调用给重置为RPL=3,非法访问操作系统内核数据段的企图将会被CPU发现,引发异常中断(CPL=0 RPL=3,DPL=0 校验不通过)。

五、内存特权级保护校验规则

  前面的举例分析中,或多或少的介绍了几种内存访问时的特权级保护校验规则,这里系统的总结一下。内存特权级保护的规则根据内存段的性质不同,有一定差异,分情况讨论。

代码段特权级校验规则

  非特权级依从代码段直接转移: 直接控制转移到非特权级依从的代码段时,要求当前特权级CPL、请求特权级RPL都等于目标代码段DPL(数值上CPL = 目标代码段DPL,RPL = 目标代码段DPL)。

  特权级依从代码段直接转移:直接控制转移到特权级依从的代码段时,要求当前特权级CPL、请求特权级RPL都低于或等于目标代码段DPL(数值上CPL >= 目标代码段DPL,RPL >= 目标代码段DPL)。

  门描述符特权转移:通过门描述符进行的控制转移规则较为复杂,在后续关于门描述符的博客再对门描述符控制转移的规则再进行展开介绍。

数据段特权级校验规则

  CPU允许高当前特权级的程序访问低特权级别的数据段。换句话说,低当前特权级的程序无法访问高特权级的数据段。

  即要求当前特权级CPL,请求特权级RPL都必须高于或等于目标数据段DPL(数值上CPL <= 目标数据段DPL,RPL <= 目标数据段DPL)

栈段特权级校验规则

  CPU要求任何时候,访问的栈段特权级必须和当前特权级CPL相一致。

  即要求当前特权级CPL,请求特权级RPL都必须等于目标栈段DPL(数值上CPL = 目标栈段DPL,RPL = 目标栈段DPL)。

六、I/O特权级保护

  一般来说,操作系统是不允许应用程序有权限直接访问外设的,而是通过提供系统调用的方式对外设的访问进行保护。在标志寄存器EFLAGS中,第12、13位是IOPL位(I/O Privilege Level),代表着当前任务的I/O特权级,只有当前特权级为0的任务才能修改标志寄存器中的IOPL位。IOPL共两位,对应着特权级的四个级别。当前特权级CPL高于或等于IOPL的时候(数值上CPL<=IOPL),便允许当前任务访问外设。通过IOPL,操作系统便能按照需要对不同的应用程序进行外设访问的控制。

  (截图自 x86汇编语言 从实模式到保护模式)

  但有时操作系统并不希望彻底的阻止某一应用程序对外设的访问,而是能选择性的对应用程序开放部分I/O端口的访问权限。因此80386还提供了I/O访问位图用于更细粒度的外设访问权限控制。

  80386最多可以访问2^16共65536个外设端口。对应的,I/O访问位图中按照从零开始顺序的每一bit位标识了当前任务是否有权限访问对应的I/O端口,第一个bit位代表端口0,第二个bit为代表端口1,以此类推,最大可以为65536位,占8Kb空间。

  I/O访问位图中的bit位为0代表对应端口有权限,bit位为1代表对应端口无权限。当CPU发现CPL低于IOPL时,并不会直接发生异常,而是会进一步尝试着查询当前任务的I/O访问位图,只有在I/O访问位图中也发现不满足条件时,才产生异常。通过I/O访问位图,操作系统可以灵活的设置应用程序的I/O端口访问权限,选择性的向其开放所需要的接口权限。

  需要注意的是,端口是面向字节编址的,每个端口仅被设计为一次读写一个字节的数据。当以字、或者双字进行端口访问时,实际上是访问连续的2个或4个端口。当从端口n读取一个字,即两个字节时,相当于从端口n和端口n+1中各读取一个字节的数据。因此,当CPU进行字、双字的I/O指令时,会同时检查相关的2个或4个连续的端口。只有当I/O位图中对应的所有的端口标识都为0时(有权限),才通过权限校验。

七、总结

  在通过学习《x86汇编语言 从实模式到保护模式》以及有关内容的博客,掌握了一定的80386硬件及汇编知识后,我才具备了阅读、学习ucore操作系统源码的基础。虽然《x86汇编语言 从实模式到保护模式》的作者在很多地方都很体贴的站在初学者的角度来讲解原理,但一方面由于自己在阅读时不够专注,另一方面也和涉及到的知识点繁多且关系紧密有关。对我而言,单纯通过阅读学习的效果并不是特别理想。 

  《暗时间》一书中提到了两个很有价值的观点:书写是为了更好的思考、教是更好的学。在写博客的过程中,会不自觉的反复思考并总结所写的内容,找到那些自以为了解但事实上却理解不够深刻的内容。同时假设有一个虚拟的初学者会阅读所写的博客,换位思考这个虚拟的初学者可能遇到的问题,尽可能的将内容以浅显易懂的方式说清楚。当然,如果博客能帮助到和我一样,对CPU硬件、操作系统原理感兴趣的小伙伴就更好了。

  对于学习基于x86保护模式的ucore乃至流行的Linux、Windows等操作系统来说,x86CPU的特权级保护机制是至关重要的一部分。有了硬件级别的特权级保护机制,才能实现多任务的隔离以及各种软件级别的权限控制。后续的80386学习相关博客将会包括门描述符的详细介绍以及分页机制、虚拟内存管理等相关的知识点,这些内容都是学习ucore操作系统公开课时所必须的。

  作为一个初学者,博客中如有错误或者理解不到位的地方,还请指正。

posted on 2020-05-17 22:23  小熊餐馆  阅读(1691)  评论(1编辑  收藏  举报