程序员在用户程序开发过程中,会遇到两个基本概念即用户态和内核态,我们所说的模式切换,就是用户态和内核态之间的切换。
用户态和内核态其实是CPU的特权级,所以模式的切换就是CPU特权级的切换,模式等同于特权级,不同的模式表示CPU处于不同的特权级下,因此CPU特权级的切换不能局限于用户态到内核态之间,理论上CPU可以在任何特权级之间互相切换。
CPU特权级表示处理器访问计算机资源时,CPU所处的特权级即CPL,CPU处于不同的特权级,它能访问的计算机资源范围不同,计算机资源包括内存段(代码段,数据段,栈段),IO设备,核心数据结构。
特权级根据权限大小,分为4个等级即0,1,2,3,如下图
特权级
由上图所示,特权级越小,它对应的数值越大,用户程序通常被赋予3级特权,CPU访问它时,CPU的特权级CPL为0,1,2,3就可以了,当CPU需要访问更高特权级的计算机资源时,CPU就需要进行特权级切换,使得CPU处于更高的特权级下,内核被赋予0级特权,CPU如果需要访问内核时,CPU的特权级CPL必须为0,系统程序被赋予的特权级在内核和用户程序之间,系统程序指的是设备驱动程序,虚拟机等一些系统服务。
由上文所述,每个计算机资源都被贴上一个标签即特权极,我们称这种特权极为计算机资源特权极,贴上标签后,只有当CPU特权极CPL被切换为比这个计算机资源特权极更高或者同等特权极时,才能访问这个计算机资源,例如某个计算机资源的特权极是2,那么CPU访问这个资源时,CPU特权极CPL至少和2相同,即数值上不能大于它,CPU特权极CPL必须处于即2,1,0时才可以访问。
我们把计算机资源特权极根据资源类型不同分为内存段特权极,IO设备特权极,核心数据结构存储在内存中,操作系统把它存储在数据段中,不过有的核心数据结构被赋予单独的特权极,其它的核心数据结构的特权极同它所在的数据段的特权极相同,因此我们总结为:计算机资源的特权极分为内存特权极和IO设备特权极,内存特权极又分为内存段特权极和核心数据结构特权极。
本篇文章基于linux操作系统保护模式,Inter x86处理器架构,主要是为了理解CPU特权极和计算机资源特权级的原理,至于其他操作系统和处理器的特权极,思路上相似,实现细节上各有不同罢了。
为了减少篇幅,便于消化理解,我会拆分为三篇文章,分为上中下三个部分,分别阐述特权极的不同话题。
计算机资源特权级之-内存特权级(DPL)
CPU特权级(CPL)的切换过程
计算机资源特权级之-IO设备特权极(IPL)
计算机资源特权级之内存特权极
开发人员编写的用户程序通常会进行系统调用,而系统调用的代码和数据在内核,因此一个完整的应用程序不仅包括开发人员编写的代码和数据,也包括内核的代码和数据,如下图
一个完整的应用程序
由上图得知,一个程序无论是用户程序,系统程序,内核都至少包括3个部分即栈段,数据段,代码段。
代码段存储程序编译后的机器指令,数据段存储代码段中用到的数据,例如全局变量,常量,静态变量等,栈段存储着栈结构数据(STACK),在函数调用过程中会动态创建和销毁栈结构,栈结构存储着当前函数的参数,局部变量,返回地址等,栈结构初始大小为0,随着函数的调用动态扩大和缩小,但所有的栈结构空间加起来不能超过栈段的大小。
如果用户程序请求其它特权级下的计算机资源,势必会调用其他特权级下代码段,数据段,栈段,一旦调用了,操作系统就会把其他特权级下的代码段,数据段,栈段链接进来,因此广泛地说,一个完整的用户程序可能包括所有特权级下的代码段,数据段,栈段,因此每个用户程序下的每个特权级下有各自独立的代码段,数据段,栈段,例如对于栈来说,用户态下是用户栈,内核态是内核栈,系统程序是系统程序栈。
内核代码段,数据段,栈段在计算机启动的时候,由内核自己把它的内核代码段,数据段,栈段设置为特权级0,其它特权级下的代码段,数据段,栈段统统由操作系统从文件中加载到内存,然后在内存中分配固定大小的代码段,数据段,栈段,并给这些内存段设置不同的特权极,也就是说它们的特权级是由操作系统决定的,那么这些计算机资源的特权级存储在哪里呢?
特权级存储在一个叫做描述符的结构里,如下图所示。
描述符层次结构
如上图所示,每个描述符占用8个字节,描述符分为段描述符,门描述符,而门描述符又分为任务门描述符,调用门描述符,中断门描述符,陷阱门描述。
描述符又存储在哪里呢?答案是存储在描述符表中,而描述符表又存储在内存中的一个特殊的数据段中,每个描述符表可以存储8192个描述符,那么一个描述符表最多占用的空间就是8192 *8,等于65536个字节即64kb,描述符表分为三种,如下图:
描述符表层次结构
由上图所示,描述表分为三类即中断描述符表(IDT),全局描述符表(GDT),局部描述符表(LDT)。
GDT和LDT中可以存储段描述符,调用门描述符,任务门描述符,IDT中可以存储调用门描述符,任务门描述符,中断门描述符,陷阱门描述符,可见调用门描述符和任务门描述符可以存储在上图中的三种描述符表中,中断门描述符和陷阱门描述符只能存储在IDT中,段描述符只能存储在GDT和LDT中。
GDT和LDT唯一的区别就是:是否全局的,GDT是全局的,所有程序共享的,LDT是局部的,可能只属于某个程序内部,IDT也是全局的,通常操作系统分配的内存段例如代码段,数据段,栈段的段描述符都存储在GDT中,为了便于理解,后文不再区分GDT和LDT,统一按照GDT阐述。
那么我们如何定位一个描述符呢,我们已经知道了描述符存储在描述符表中,那么我们可以把描述符表看做一个数组,这个数组的元素是描述符,那么我们只需要知道数组的起始地址和数组的索引,就可以定位一个描述符,数组的起始地址即描述符表的起始地址已经由操作系统存储在专门的寄存器了,索引在哪里呢?对于IDT,它的索引是中断相量号,对于GDT来说,它的索引是选择子,选择子长度为16位,其中高13位为GDT数组的真正索性,低2位是一个叫RPL的东东,大致意思是请求特权极,点到为止,后面会详细介绍,第3位不再介绍,无关轻重。两张图可以解释描述符定位过程,如下图:
对于中断描述符表(IDT)定位描述符
由上图所示,对于中断描述符表(IDT),它的起始地址存储在IDTR寄存器中,中断向量号是一个无符号整数,因为描述符占用8个字节,所以我们只需要IDTR寄存器中的起始地址加上中断向量号*8就可以得出描述符在内存中的位置。
对于全局描述符表(GDT)定位描述符
由上图所示,对于全局描述符表,它的起始地址存储在GDTR寄存器中,选择子的高13位是真正的索引,因为每个描述符占用8个字节,所以我们只需要GDTR寄存器中的起始地址加上真正索引*8就可以得出描述符在内存中的位置。
好了,描述符和描述符表的分类,以及描述符的定位过程介绍完了,下面我们来介绍下描述符即段描述符和门描述符。
段描述符:
操作系统加载程序和数据后,会在内存中分配代码段,数据段,栈段,他们的大小是固定的,操作系统分配好3个内存段后,会建立3个段描述符即代码段描述符,数据段描述符,栈段描述符,段描述符的简化版格式如下:
段描述符简化版
由上图所示,为了便于理解,整理了一个段描述符的简化版本,我们逐个看起
段的起始地址:就是操作系统分配的内存段的起始地址。
段特权级DPL_SEG:操作系统根据程序类型来分配的,例如对于用户程序,操作系统为它赋予特权极3,而设备驱动程序,操作系统为它赋予特权极很可能就是1,这是本篇最重要的概念之一,它表示了CPU访问这个内存段时,CPU特权级(CPL)要高于或者等于它(DPL_SEG),假如DPL_SEG为2,那么CPU特权级(CPL)要想访问这个段,CPL必须高于或者等于2即数值上小于等于2,也就是说,0,1,2这三种CPU特权级(CPL)都可以访问它。
段权限:包括可读,可写,可执行,是不是一致性代码段,是否被CPU访问过等,总共占用了4位,通过任意的权限组合,可以区分出一个段是代码段还是数据段,例如代码段通常是可执行,可读,不可写,另外代码段根据是不是一致性分为:不一执行代码段和一致性代码段,大部分的代码段都是非一致性的,一致性的代码段是干啥的,先不管它,后面说到CPU特权级切换时再说。
段大小:就是一个段的大小,表示了一个段从起始地址到段结束地址之间的范围。
其它:其它的位置如E位,表示段是向上或者向下扩展,通过这个位可以区分一个段是数据段还是栈段,向下扩展表示是栈段,向上扩展表示是数据段。
好了,段描述符介绍完了,再来看看门描述符。
门描述符
门描述符分为任务门描述符,调用门描述符,中断门描述符,陷阱门描述的,门描述主要包括两部分,如下图为门描述的简化版本
第一部分是一个处理程序的地址,我们可以把处理程序理解为一个函数,第二部分是这个门描述符的特权极,我们叫它DPL_DOOR。
我们上文说过,内存特权极分为内存段特权极和核心数据结构特权极,门描述符就是核心数据结构,它有自己独立的特权级,我们称它为DPL_DOOR即门特权极。
这里可以简单总结下:内存特权级分为内存段特权级(DPL_SEG,它在段描述符中)和门特权级(DPL_DOOR,它在门描述符中),内存段特权级(DPL_SEG)表示CPU访问这个内存段时,CPU特权级(CPL)必须高于或者等于DPL_SEG,从数值的角度来看即CPL<=DPL_SEG,门特权级(DPL_DOOR)表示CPU访问门描述符时,CPU特权级(CPL)必须高于或者等于DPL_DOOR,从数值的角度来看即CPL<=DPL_DOOR,无论内存段还是门描述符都属于计算机资源,通常访问这些资源时,CPU特权级高于或者等于这些资源所需的特权级也是正常的,从这个角度理解就可以了。
门描述符的作用就是定位一段处理程序,对于调用门描述符,中断门描述符,陷阱门描述,它们定位这段处理程序是通过选择子和处理程序偏移量,我们知道一个选择子可以定位到一个段描述符,这个段描述符存储了内存段的起始地址,对于处理程序来说,它是存储在代码段中的,因此知道了选择子就可以知道一个代码段的起始地址,然后处理程序不总是在代码段的起始位置,它通常需要一个偏移量,因此代码段的起始地址加上这个偏移量就定位到了处理程序的起始地址,对于任务门描述符,没有偏移量,它的偏移量存储在其它地方,我们后续不会再阐述任务门,因此不再对它进行介绍。
那么段描述符和门描述符的区别是什么呢,我们大概已经理解了吧,段描述符描述一个内存段的起始地址,大小,段特权级(DPL_SE),权限等,内存段可以是代码段,数据段,栈段,这个内存段里面存储什么,并不关心,而门描述符则描述了一段具体的处理程序,这段具体的程序肯定在代码段中的指定位置(因此需要选择子和偏移量),并且已经确定它是实现什么功能了,例如一个中断门描述符可以指向缺页中断处理程序,这个缺页中断处理程序只负责缺页的相关逻辑。
关于内存特权级阐述到这里了,我们来总结下吧
总结
每个计算机资源例如本文阐述的内存段(数据段,代码段,栈段)和核心存储结构(门描述符)都被操作系统打上了特权级标签,我们称这些内存资源的特权级为DPL,在文章里内存段特权级我们叫做段特权级(DPL_SEG),门描述符的特权级我们叫做门特权级(DPL_DOOR),通常我们访问内存段或者门描述符时,CPU特权级(CPL)必须高于或者等于内存特权级DPL(DPL_SEG和DPL_DOOR),从数值的角度看CPL<=DPL(DPL_SEG和DPL_DOOR)。
我们开发的程序中的代码,数据被操作系统加载后,会在内存中创建内存段即代码段,数据段,栈段,然后针对每个段创建一个段描述符,存储在全局描述符表中(GDT),我们根据段描述符在GDT中的索引和段特权级DPL_SEG,创建选择子,选择子的高13位存储索引,低2位存储请求特权级(RPL),然后将3个选择子存储在内存中。
RPL为请求特权级,通常我们会通过请求一个选择子,然后根据选择子获取对应的描述符,然后根据描述符去访问内存段资源和门描述符,那么RPL就表示选择子是由哪个特权级下的代码请求的,可以是操作系统,也可以是用户程序,通常一个选择子的RPL等于这个选择子指向的描述符中的特权级(DPL(DPL_SEG和DPL_DOOR)),RPL可以被修改,可以是善意的修改,也可以是恶意的修改,就先讲到这里,RPL的其它阐述就放在特权级切换文章中阐述,这里不再深究。
当CPU要执行某段代码时,操作系统会将代码段选择子加载到CS寄存器中,然后CPU可以根据CS寄存器中的选择子查找GDT,找到段描述符,然后根据段描述符找到代码段的起始地址,然后就可以从起始地址处执行代码了,在代码的执行过程中,会用到全局变量数据,常量或者静态数据,那么操作系统会将该程序的数据段选择子加载到DS寄存器中,然后CPU可以根据DS寄存器中的选择子查找GDT,找到了段描述符,然后根据段描述符找到数据段的起始地址,再加上指令中操作数(数据的偏移量),定位到数据,随着函数或者处理程序的不断调用,会涉及到出栈和入栈,因此操作系统会将该程序的栈段选择子加载到SS寄存器中,然后CPU可以根据SS寄存器中的选择子查找GDT,找到了段描述符,然后根据段描述符找到栈段的起始地址,然后创建栈结构,栈结构是可以随着函数的调用嵌套的,也可以随着一个函数的返回而销毁,每个栈结构都有个指针即栈顶指针,栈顶指针存储在栈指针寄存器中(ESP),用于指向当前栈的栈顶,还有一个指针即栈底指针,用于表示一个栈结构的起始位置,它存储在栈底寄存器(EBP),对于一个栈结构来说EBP是固定不变的。
那么门描述符干啥用的呢,门描述符用于指向一段处理程序,这段处理程序实现某个具体功能,那么门描述符的具体用途是什么呢,我们可以先留个底,门描述符的用途就是实现系统调用,再广泛一点就是实现不同特权级下切换的。
好了,内存特权级介绍这里了,下篇文章将重点阐述不同特权级下的代码和数据如何切换,需要消化本篇文章的概念后,才能理解下一篇文章。
番外篇
段描述符是在操作系统加载程序时,创建内存段的同时,创建了段描述符和选择子,各类门描述符是操作系统启动的时候,自动创建的并且内置到GDT或者IDT中的。