代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
1.概述
CPU负责维护计算机内的安全,它将程序拥有的权力分为4个等级:0、1、2、3;数字越小,权力越大。这就是保护模式下特权级的由来。
0特权级是OS内核所在的特权级,必须让OS处于至高无上的地位。计算机在启动之初就是以0特权级运行的,MBR是我们写的第一个程序,自从它从BIOS那里接过第一棒的时候,它就已经处于0特权级了。
OS位于最内环的0特权级,它直接控制硬件,掌控各种核心数据。
系统程序分别位于1和2特权级,运行在这两层的程序一般是虚拟机、驱动程序等系统服务。
3特权级的是用户程序,用户程序被设计为“有需求时找OS”。

2.TSS简述
定义:Task State Segment,任务状态段,它是处理器在硬件上原生支持多任务的一种实现方式。
TSS是每个任务都有的结构,它用于一个任务的标识,相当于任务的身份证,程序拥有此结构才能运行。

在没有OS的情况下,进程就是任务,任务就是一段在CPU上运行的程序;在有了OS之后,程序可以分为用户程序和操作系统内核程序。因此,一个任务按照特权级划分的话,是分为0特权级和3特权级两部分。完整的任务要经历这两种特权级的变换。
任务是由CPU执行的,任务特权级的变换,实际上是CPU的当前特权级在变换。
处理器在不同特权级下,应用不同特权级的栈。
一个任务在每个特权级下只能有一个栈,当CPU进入不同的特权级时,它自动在TSS中找同特权级的栈。
而TSS中只有三个栈:ss0和esp0、ss1和esp1、ss2和esp2。分别代表0、1、2特权级的段选择子和偏移量。
特权级转移分类:
- 从低特权级到高特权级:由中断门、调用门实现;由于不知道目标特权级对应的栈地址在哪,所以提前把目标栈的地址记录在TSS中,当处理器向高特权级转移时会自动从中取出加载到SS和ESP中更新栈。所以TSS中记录的栈是转移后的高特权级的目标栈,因此TSS中不需要记录3特权级的栈,因为是最低级的栈,没有特权级会向它转移。
- 从高特权级到低特权级:由返回指令实现;只有先向更高特权级转移后,才能回到低特权级。所以当CPU从低向高特权级转移时,会将当前低特权级压入转移后高特权级所在的栈中。
CPU如何找到TSS:TSS是硬件支持的系统数据结构,它和GDT一样,由软件填写其内容,由硬件使用。GDT也要加载到寄存器GDTR中才能被处理器找到,TSS是由寄存器TR(Task Register)加载的,每次CPU处理不同任务时,将TR寄存器加载不同任务的TSS就可以了。
3.CPL和DPL以及RPL
计算机特权级的标签体现在DPL、CPL和RPL。
RPL
RPL(Request 请求特权级)
选择子的0~1位就是RPL字段,它就是请求特权级。即RPL
在计算机中,具有“能动性”的只有计算机指令,只有指令才具备访问、请求其他资源的能力。指令“请求”“访问”其他资源的能力等级称为“请求特权级”,指令放在代码段中。
代码段寄存器CS中选择子的RPL位表示代码请求别人资源能力的等级,它又称为处理器的当前特权级,即处理器的当前特权级为CS.RPL。
RPL是通过段选择子的第0和第1位表现出来的。RPL是代码中根据不同段跳转而确定,以动态刷新CS里的CPL,在代码段选择符中。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。操作系统往往用RPL来避免低特权级应用程序访问高特权级段内的数据,即便提出访问请求的段有足够的特权级,如果RPL不够也是不行的,当RPL的值比CPL大的时候,RPL将起决定性作用。也就是说,RPL相当于附加的一个权限控制,只有当RPL>DPL的时候,才起到实际的限制作用。
CPL
CPL(Current 当前特权级)
CPL是当前执行的程序或任务的特权级。它被存储在CS和SS的第0位和第1位上。通常情况下,CPL代表代码所在的段的特权级。当程序转移到不同特权级的代码段时,处理器将改变CPL。只有0和3两个值,分别表示用户态和内核态。
在CPU中运行的是指令,其运行过程中的指令总会属于某个代码段,该代码段的特权级,也就是代码段描述符中的DPL,便是当前CPU所处的特权级,这个特权级被称为当前特权级CPL,表示CPU正在执行的代码的特权级。
当前特权级CPL保存在CS选择子中的RPL部分。
RPL变为CPL,其实只是代码段寄存器CS中的RPL是CPL,其他段寄存器中选择子的RPL与CPL无关。因为CPL是针对具有“能动性”的访问者而言的,代码是访问的请求者,所以CPL只存放在代码段寄存器CS中低2位的RPL中。
DPL
DPL(Descriptor 描述符特权级)
DPL表示段或门的特权级。它被存储在段描述符或者门描述符的DPL字段中,当前代码段试图访问一个段或者门(这里大家先把门看成跟段一样),DPL将会和CPL以及段或者门选择子的RPL相比较,根据段或者门类型的不同,DPL将会区别对待。
当前正在运行的代码所在的代码段的特权级DPL就是处理器的当前特权级,当处理器从一个特权级的代码段转移到另一个特权级的代码段上执行时,由于两个代码段的特权级不一样,处理器当前的特权身份起了变化,这就是当前特权级CPL改变的原因。
其实就是使用了那些能够改变程序执行流的指令,如int、call等,这样就使CS和EIP的值改变,从而使处理器执行到了不同特权级的代码。
不过特权转移并不是随便转移的,那么设置这个特权级的意义就没有了,处理器要检查特权变换的条件。当处理器特权级检查的条件通过后,新代码段的DPL就变成了处理器的CPL,也就是目标代码段描述符的DPL将保存在代码段寄存器CS中的RPL位。
我们的代码中,再打开保护模式前,CS寄存器的值一直为0。在打开了保护模式后,进行了一次远跳程序刷新流水线,其段选择子为SELECTOR_ CODE,这个选择子的特权级为0,也就是说一进入保护模式,我们的当前特权级就是0了。远跳时,由于是从0到0,才能成功。
DPL在段描述符中占2位,可以表示4个组合:00b、01b、10b和11b。
DPL是段描述符所代码的内存区域的“门槛”权限,访问者能否迈过此门槛访问到本描述符所代表的资源,其特权级至少要等于这个门槛。
在计算机中真正的访问者是硬件CPU,而指挥CPU行为的是具有可执行能力的指令代码,所以访问者就是代码段中的指令。
对于受访者为数据段(段描述符中type字段中未有X可执行属性):只有访问者的权限大于等于DPL才可访问。例如:若DPL = 1,则只有特权级为0和1的才可以访问。
对于受访者为代码段(段描述符中type字段中含有X可执行属性):只能平级访问,特权级大于小于 DPL的都不行,只有特权级 = DPL的才可以访问。对于受访者为代码段而言,实际上是指处理器从当前运行的代码段上转移到受访者这个目标代码段上去执行。
代码段不平级运行的方法:
唯一一种CPU会从高特权级变低特权级运行的情况:CPU从中断处理程序中返回到用户态的时候。中断处理是在0特权级下运行的,因为中断的发生多半是外部硬件发生了某种状况/不可抗事件而必须通知CPU导致的,所以中断的处理过程中需要具备访问硬件的能力。再者,有些中断处理中需要的指令只能在0特权级下使用,这部分指令被称为特权指令。在运行用户程序时若发生了中断,CPU会暂停用户程序的执行,随后CPU会自动由3特权级进入0特权级,在0特权级下将执行用户程序时的现场环境(上下文)保存起来,待中断处理完成后,CPU会恢复用户程序的执行,回到3特权级。
CPU提供了几种用于从低特权级变高特权级的方法:
- 一致性代码:一致性代码段是指如果自己是转移后的目标段,自己的特权级(DPL)一定要大于等于转移前的CPL。也就是说一致性代码段的DPL是权限的上限,任何在此权限之下的特权级都可以转到此代码段上执行。一致性代码的特点是转移后的特权级不与自己的特权级(DPL)为主,而是与转移前的低特权级一致,听从、依从转移前的低特权级,并不会将CPL用目标段的DPL替换。在一致性代码的特权级检查中,RPL不参与。特权级检查发生在访问者访问受访者的一瞬间,只检查一次,检查过后,在该段上以后的执行过程中再也不会被检查。
- CPU通过“门结构”从低特权级变高特权级。
4.门
- RPL的产生主要是为了解决系统调用时的“缺权”问题,系统调用的实现方式中,以调用门和中断门最为合适。
- 门结构是记录一段程序起始地址的描述符。
- 门描述符同段描述符类似,都是8字节大小的数据结构,用来描述门中通向的代码。
- 门描述符一共有4种:任务门描述符、中断门描述符、陷阱门描述符和调用门描述符。
结构如图所示:




门描述符与段描述符最大的不同在于除了任务门外,其他三种门都是对应到一段例程,即对应一段函数,而不是像段描述符对应的是一片内存区域。任何程序都属于某个内存段,所以程序确切的地址必须用“代码段选择子+段内偏移量”来描述,可见,门描述符基于段描述符,例程是用段描述符来给出基址的,所以门描述符中要给出代码段的选择子,但光给出基址远远不够,还必须给出例程的偏移量,这就是门描述符中记录的是选择子和偏移量的原因。
任务门描述符可以放在GDT、LDT和IDT(中断描述符表),调用门可以位于GDT、LDT中,中断门和陷阱门仅位于IDT中
任务门、调用门都可以用call和jmp指令直接调用,原因是这两个门描述符都位于描述符表中,要么是GDT,要么是LDT,访问它们同普通的段描述符是一样的。陷阱门和中断门只存在于IDT中,因此不能主动调用,只能由中断信号来触发调用。
门的调用:
- 调用门:call和jmp指令后接调用门选择子为参数,以调用函数例程的形式实现从低特权级变成高特权级转移,可以来实现系统调用。call指令使用调用门可以实现向高特权级代码转移,jmp指令使用调用门只能向平级特权代码转移。
- 中断门:以int指令主动发中断的形式从低特权级变到高特权级转移,Linux的系统调用便是以中断门实现的。
- 陷阱门:以int3指令主动发中断的形式实现从低特权级变到高特权级转移,这一般是编译器在调试时用的。
- 任务门:以任务状态段TSS为单位,用来实现任务切换,它可以借助中断/指令发起。当中断发生时,如果对应的中断向量号是任务门,则会发起任务切换。也可以像调用门那样,用call/jmp指令后接任务门的选择子/任务TSS的选择子。
门的“门槛”是访问者特权级的下限,访问者的特权级再低也不能比门描述符的DPL低,否则连门都进不去,更谈不上调用门。
门的“门框”是访问者特权级的上限,访问者的特权级再高也不能比门描述符中目标程序所在的代码段的DPL高。
在使用门结构前,CPU要例行公事做特权级检查,参与检查的是CPL、DPL和RPL。处理器从一个特权级转移到另一个特权级,任何时刻CPU所处的特权级称为当前特权级CPL。CPL变换的原因是CPU从某一特权级的代码段转移到另一特权级的代码段上运行,代码段的特权级DPL就是未来CPU的CPL。各种门结构存在的目的就是为了让CPU提升特权级,这样CPU才能完成一些低特权级无法完成的任务。
5.调用门
调用门结构
OS可以利用调用们实现一些系统功能(现代OS一般用中断门实现系统调用),用户程序需要系统服务时可以调用该调用门以获得内核帮助。

既然门描述符用来指向某个内核例程,是例程就需要参数,那么在用户3特权级下如何向0特权级下的内核传递参数呢?不同特权级下CPU用不同的栈,在用户进程下,参数压入3特权级的栈。为了让0特权级的内核可以使用参数,CPU在硬件上实现参数的自动复制,将用户进程压在3特权级栈中的参数自动复制到0特权级栈中。
在图中,调用门结构中有个“参数个数”的变量,这是CPU将用户提供的参数复制到内核时需要用到的。因为参数在栈中是挨着的,所以只需要知道个数即可。

调用门的过程保护
调用门涉及两个特权级,一个是转移前的低特权级,也就是程序调用“调用门”时的CPL;另一个是转移后的目标特权级,这是由门描述符中选择子对应的目标代码段的DPL决定的。
call指令调用“调用门”的完整过程:该门描述符中参数个数为2,即用户进程要为调用门提供2个参数。调用前的当前特权级为3,调用后的新特权级为0。
为此调用门提供2个参数,这是在使用调用门之前完成的。目前是3特权级,所以要在3特权级栈中压入参数。
确定新特权级使用的栈。新特权级就是未来的CPL,也就是转移后的目标代码段的DPL。CPU自动在TSS中找到合适的栈段选择子ss和栈指针esp作为转移后的新栈。记作SS_new和ESP_new。
检查新栈选择子对应的描述符的DPL和TYPE,如果未通过检查则CPU引发异常。
若转移后的目标代码段的DPL比CPL高,说明需要特权级转换。将旧栈段选择子SS_old和ESP_old保存到新栈中,这样在高特权级的目标程序执行完后才能通过retf返回旧栈中。但因为只有使用新栈后才能将SS_old和ESP_old保存到新栈中,所以CPU先找个地方存放SS_old和ESP_old,再将SS_new加载到栈段寄存器SS,ESP_new加载到栈指针寄存器ESP,开启新栈。
在使用新栈后,将上一步中临时保存的SS_old和ESP_old压入新栈中【用于恢复栈,如果是平级转移则不需要保存旧栈,因为压根不需要换栈】。
将用户栈中的参数也复制到新栈中,根据“参数个数”决定复制的参数。
由于调用门描述符中记录的是目标程序所在的代码段的选择子和偏移地址,这意味着代码段寄存器CS要用该选择子重新加载。只要栈寄存器被加载,段描述符缓存寄存器就会被刷新,相当于切换到了新段上运行,这是段间远转移。所以要将当前代码段CS和EIP都备份在栈中,分别记作CS_old和EIP_old。这两个值是恢复用户进程的关键,也就是从内核进程返回时用的地址【用于恢复代码段】。
一切就绪,只差运行调用门中指向的程序了。把门描述符中的代码段选择子装载到代码段寄存器CS中,然后把偏移量装载到指令指针寄存器EIP中。至此,CPU终于从用户程序转移到内核程序上,实现了特权级由3到0的转移。

如果从高特权级返回低特权级,用retf指令将返回地址从栈中弹出到CS和EIP,将低特权级栈地址弹出到SS和ESP中。
利用retf指令从调用门返回的过程:
- 将CPU执行retf时,知道这是远返回,所以需要从栈中返回旧栈的地址及返回到低特权级的程序中。这时它要进行特权级检查,先检查栈中CS选择子,根据其RPL位,即未来的CPL,判断在返回过程中是否需要改变特权级。
- 此时栈顶应该指向EIP_old,从栈顶弹出CS_old和EIP_old。根据CS_old选择子对应的代码段的DPL及选择子中的RPL做特权级检查。若检查通过,则将EIP_old弹出到寄存器EIP,CS_old的低16位弹出到寄存器CS中。
- 如果返回指令retf后面有参数,则增加栈指针ESP_new的值,以跳过栈中参数。按理说retf+参数就是为了跳过低特权级复制到高特权级栈中的参数,所以retf后面的参数应该等于参数个数*参数大小。
- 如果第一步中判断出需要改变特权级,则将ESP_old弹出到寄存器ESP中,SS_old的低16位弹出到寄存器SS中,恢复旧栈。
jmp只能用在不需要特权级变化,且不从调用门返回的场合。
6. RPL
为了系统安全必须保证的两个客观条件:
- 用户不能访问系统资源,不能越俎代庖去做OS的事情。
- CPU必须要陷入内核才能帮助用户程序做”大事”,所以CPU的当前特权级会变成至高无上的0特权级。
因此,受访者不知道访问者的真实身份(OS还是用户程序),只能知道它的特权级。
RPL为请求特权级,它代表真正请求者的特权级。以后在请求某特权级为DPL级别的资源时,CPL和RPL的特权必须同时大于等于受访者的DPL。
用户程序的CPL不可伪造,它起始是由OS在加载用户程序时赋予的,记录在段寄存器CS中的低2位,也就是RPL的位置,而CS寄存器只能通过call、jmp、ret、int和sysenter等指令修改。如果用户程序申请了OS服务,如果它提交选择子做参数,选择子中的RPL也会被OS修改为用户进程的CPL。
arpl指令可以修改选择子中的RPL。
指令格式:arpl 同样寄存器/16位内存,16位通用寄存器。
实际此指令操作数变成了:arpl 用户提交的选择子,用户段寄存器CS的值
特权级检查在什么时候发生?如何被触发?
1.CPU的特权检查,都只发生在往段寄存器中加载选择子访问描述符的那一瞬间。
2.若不通过调用门、直接访问一般代码时的特权检查规则:
- 若目标为非一致性代码,则CPL = RPL = 目标代码段的DPL。
- 若目标为一致性代码,则特权级的CPL和RPL <= DPL(特权级,非数值)。
- 有关代码的特权检查,都发生在能够改变代码段寄存器CS和指令指针寄存器EIP的指令中。例如:call、jmp、ret 和 sysexit等。
3.若不通过调用门、直接访问一般数据时的特权检查规则:
- CPL和RPL在特权上 >= 目标数据段的DPL
- 特权级检查会发生在向数据段寄存器中加载段选择子时。数据段寄存器包括DS和附加段寄存器ES、FS和GS。
- 栈段的检查比较特殊,因为在各特权级下,CPU都要对应的栈,所以栈的等级要与CPL相等。所以往段寄存器SS中赋予数据段选择子时,CPU要求CPL = RPL = 栈的目标数据段的DPL。
- 例如:mov ds,ax时便会触发特权级检查。ax中的值被当作选择子,CPU会拿ax中的低2位,即RPL和CPL分别与ax中选择子所指向的段描述符DPL作比较。如果满足RPL和CPL等级都 >= DPL时,选择子才会被加载到DS中。
CPL和RPL的区别:
- CPL是对当前正在运行的程序而言的,而RPL有可能是正在运行的程序,也可能不是。
- 如果低特权级不向高特权级程序提供自己特权级下的选择子,也就是不涉及向高特权级程序“委托、代理”办事的话,CPL和RPL都出自同一程序。
- 如果低特权级向高特权级“委托、代理”办事的话,CPL是指代理人,即内核;RPL则有可能是委托者,即用户程序,也可能是内核自己。
- 例如:若用户程序运行在3特权级,它想通过调用门读取硬盘上某个文件到它自己的数据缓冲区中。它需要提供3个参数:文件所在的硬盘扇区号、用于存储文件的缓冲区所在的数据段选择子以及缓冲区的偏移地址。用户进程只能把与自己同一特权级的数据段作为缓冲区,所以该缓冲区所在的段的DPL为3,其选择子的RPL为3。进入调用门后,CPU的CPL由运行用户进程时的3变为内核态的0。当内核从硬盘上读取完数据后,需要将其写入用户的缓冲区中,缓冲区的选择子是由用户提供的,其RPL为3,缓冲区所在段的DPL为3,此时CPL为0,所以写入成功。RPL是用户进程提供的,而往缓冲区中写数据时CPL指的是内核,不是同一个程序。
调用门的特权级检查:
- CPL在DPL_GATE和DPL_CODE之间。
- RPL只在进调用门时和DPL_GATE比较一下,不参与和DPL_CODE比较。因为RPL不代表真正的请求者。
假如用户程序想获取安装的物理内存大小,流程为:
- 因为用户进程的CPL = 3,不能访问DPL = 0的数据段,所以使用调用门,OS将调用门的DPL设置为3让用户进程进入门。该调用门让用户进程提交用于存储系统内存容量的缓冲区所在数据段的选择子和偏移地址。
- 用户进程的CPL与门描述符中选择子所对应的代码段描述符DPL比较,发现特权级CPL <= DPL,通过。此时CPL变为0,CPU以0特权级身份开始执行内核程序。
- 为了安全起见,OS将用户选择子的RPL变为用户进程的CPL,即指向缓冲区所在段的RPL = 3。
7. IO特权级
在保护模式下,CPU中的阶级不仅体现在代码和数据的访问中,还体现在指令中。
特权指令:特权指令的执行对计算机有着严重的影响,它们只能在0特权级下被执行。
IO敏感指令:IO读写特权是由标志寄存器eflags中的IOPL位和TSS中的IO位图决定的,它们用来指定执行IO操作的最小特权级。IO相关的指令只有在当前特权级>=IOPL时才能执行,所以它们被称为IO敏感指令。
用户进程可以访问IO端口,只是OS不允许用户进行这么做罢了。所以我们平时被灌输的思想就是用户进程无法直接访问硬件,必须借助OS的帮助。只有OS才可以访问外设,OS的职责就是管理计算机中的资源,包括软件和硬件。不允许用户进程直接访问外设,这是OS对计算机的保护。
eflags寄存器中的12~13位是IOPL,即IO特权级。它除了限制当前任务进行IO敏感指令的最低特权外,还用来决定任务是否允许操作所有IO端口。IOPL是打开所有IO端口的开关,每个任务(内核进程/用户进程)都要自己的eflags寄存器。每个任务的IOPL位表示当前任务想要执行全部IO指令的最低特权级,即CPU的最低CPL。
IO位图
- 如果CPL特权级<IOPL,也可以通过IO位图来设置部分端口的访问权限,若位 = 0表示可以访问,= 1 表示禁止访问。
- 优点:提高速度。如果所有IO端口访问都要经过内核的话,由低特权级->高特权级时需要保存上下文环境,消耗处理器时间。
- IO位图只在特区级CPL < IOPL时才生效。
- IO位图位于TSS中,可有可无,若无则表示禁止访问所有端口

IO位图大小:一共有65536个IO端口,一共需要65536/8=8192B。
TSS中偏移102自己处,占2字节空间的地方,用来存储IO位图的偏移地址。如果IO位图存在的话,它位于TSS的顶端。
TSS的实际尺寸并不固定,若存在IO位图,则TSS大小 = IO位图偏移地址 + 8192 + 1字节,结尾1B是最后的0xff;若无,则TSS大小 = 104字节。
0xFF的作用:
- 若全部位都是1,则表示禁止访问任何端口。
- 用作界限符,防止越界。
8.参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


