范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

2分钟玩转保护模式设计

  1. 指令执行1.1 单一指令片段的执行
  现代计算机,唯一能执行指令的只有CPU,CPU执行指令有下面几个阶段:IF、ID、EXEC、MEM、WB。这是执行单条指令的执行过程。通常一个程序不可能只有一条指令。而指令肯定是在内存中,那么CPU怎么知道去加载哪条指令来执行呢?所以需要  IP寄存器  来记录下一条要执行指令的地址。CPU一定有办法知道下一条指令的地址。
  CPU中寄存器的数量有限,不可能将指令执行过程中产生的数据全部保存到寄存器中,那么可以得出结论CPU一定会跟内存发生数据交换(其实是CPU的高速缓存跟内存之间进行数据交换)。那么这些数据放在内存的哪里呢?那么需要将内存按照功能进行分类:  指令段中的数据,是当前指令段执行完成之后需要销毁的数据  有些数据是多个指令之间共享的数据
  那么只要按照功能将内存存放数据进行切割即可:指令段私有数据和指令段间共享的数据,其实就是栈和堆。
  首先关注下指令流片段独享的数据,在指令片段执行完成后是需要销毁的。最理想的情况是当前正在使用的数据能比较快速的获取到,最好是O(1)复杂度,而且用完就需要销毁,这个需求非常符合栈的特性,所以通过  栈  来实现。那么如何实现一个栈呢?无外乎两种方式:连续内存和不连续内存。显然不连续的内存空间必然需要一个额外的指针记录下一片空间在哪里。
  显然,在代码段中的指令执行一定要快,不连续空间必然存在指针,需要内存间的随机寻址,肯定没有顺序寻址快,这里肯定需要使用连续空间的方式,只需要实现栈顶指针的加减来实现内存的回收操作。所以当前指令段产生的数据必然需要保存栈底和栈顶。因为指令是在CPU执行的,在CPU内部要保存栈底和栈顶只能通过寄存器来实现,这两个寄存器就是栈底寄存器(BP,base pointer)和栈顶寄存器(SP, stack pointer)。  1.2 多指令片段调用
  所有的指令都必须放在同一个文件吗?显然不可能,通过汇编器的连接操作可以将多个文件连接到一起。那么怎么实现从一个文件中的指令跳转到另一个文件呢?在写汇编指令的时候,肯定不知道另一个文件的指令地址在加载之后到底是个啥?就算知道,也得将地址写死,显然是不可能的。那要怎么解决呢?虽然不能提前知道地址是个啥,但是在加载后我们一定能够拿到每个文件加载后的地址,通过一个表来记录一下每个文件的地址,我们可以默认将每个文件加载后的内存地址记录成初始值,在指令文件加载后,直接替换为正确的值即可。在编写指令片段的时候,只需要在对应需要的地方写入文件名,链接的时候,只需要将文件名替换为同一个文件的相对地址,或者运行时修改为真实的地址即可。
  如果是同一个文件的多个指令片段,似乎是解决不了了?顺着刚刚用表来记录,我们只需要将表的粒度变得更细,给每一个指令片段命名即可,在调用的时候,传入对应指令片段的名称,通过查表找到对应的指令片段的地址就可以解决指令之间跳转,这其实就是call指令。
  从前文可知,栈可以用来保存指令片段产生的私有数据,如果只有一个栈,那么只需要通过遍历就可以拿到其他指令片段产生的私有数据,显然是不合理的,所以A调用B的过程必然会创建两个栈帧。很容易得出结论,指令段A调用指令段B,会创建B的栈帧。
  指令段B执行完后需要能够回到A的栈帧,这怎么实现呢?栈顶寄存器和栈底寄存器只有一组,只能指向当前执行的指令所对应的的指令段的栈底和栈顶。显然,我们可以先将栈A的栈底寄存器的值保存在内存中,指令段B执行结束后,用该内存中的值恢复栈A的栈帧即可。
  到这里解决了开辟栈帧的问题,但是IP寄存器只有一个,怎么指令段B执行结束之后,怎么回到指令段A中执行呢?只需要在CALL指令调用之前将当前的IP寄存器的内容保存一下,在退出之后,用保存的值恢复IP寄存器的内容就可以了。这就是RET指令的功能。
  1.3 程序调用方式CALL & RET: 这两个指令是一对,从前面一节可知,在CALL指令执行的时候会自动将当前IP寄存器(保存的下一条执行指令的地址) 保存在栈中,RET指令执行的时候,会通过之前保存的指令的地址恢复IP寄存器,这时就能恢复程序的执行状态,继续执行了。这对指令执行的过程需要手动执行栈帧的开辟和销毁操作。  ENTER & LEAVE:这两个指令是一对,可以理解为是一键操作开辟栈帧的指令。也就是栈的栈帧操作涉及到的BP,SP的操作是自动完成。LEAVE也是一样,恢复栈帧的操作是自动完成的。
  毫无疑问指令段的调用都需要使用栈来保存程序状态,传递参数,存储局部变量。CALL指令既可以支持当前的代码段(near call)的调用,也可以支持不同的代码段(far call)的调用。Near Call 通常用于在当前的程序或任务中进行跳转转。Far Call 通常用于访问OS的代码或者不同的任务间的调用。
  RET指令支持near call的返回,也支持far call的返回,除此之外,RET允许程序通过增加栈指针实现释放参数的目的。释放的字节书取决于RET N 的可选参数N的大小(N为释放字节数)。
  从图中可以看出,在调用过程中,改变了啥就需要保存啥,near call 都在当前的代码段,不需要额外保存代码段, far call 不在当前代码段,需要额外保存CS的内容。
  1.4 程序返回
  从前面学习可知,指令调用返回要依赖于栈帧。同理不同特权级返回也需要依赖于栈的保存需要的信息。在不同特权级的返回,也就far return。它不支持JMP指令的返回,因为JMP指令不会讲指令地址保存在栈中。那么怎么完成返回呢?在RET指令执行的时候,会自动从栈中执行POP信息,会符段、IP、SP的内容。
  如果要返回只有坑返回一个特权级较小的代码段,校验才会通过。(返回DPL <= DPL)。同样在恢复程序调用栈需要进行栈和代码段以及相关寄存器的恢复,同样会进行特权级校验,会利用call指令保存在栈中的数据进行恢复。  2. 分段2.1 IA-32架构发展历史
  16位处理器和分段(1978)
  IA-32架构家族延续了几代产品:16位的处理器、 8086和8088。
  8086的设计:16位的寄存器,16位的数据总线,20位地址总线(最大可访问2^20 = 1M地址空间)
  8088的设计:跟8086很类似,但是8088只包含8位的数据总线。
  8086/8088支持分段,增加了一个16位的段寄存器,段寄存器中的值是一个指向内存地址的值,这个内存空间最大支持64KB。可以同时使用4个段寄存器,所以在没有切换段的情况下,8086/8088最大的寻址防伪是256KB。20位的地址可以通过段寄存器和IP寄存器来实现,可以访问1M的地址空间。
  Intel 286处理器(1982)
  Intel286引入了保护模式,保护模式使用段寄存器来保存段选择子或者是指向描述符表的指针。描述符提供了24位的基地址:物理内存最大16M,支持虚拟内存管理和保护结构。这些保护结构包含:  段界限检查  只读和只执行段选择  4个特权级
  Intel 386处理器(1985)
  Intel 386处理器是第一个32位的处理器。开始支持32位寄存器。可以使用低16位来实现向后兼容。该处理器也提供了虚拟8086模型(为了兼容8086/8088处理器创建的可执行程序)。除此之外,Intel 386也支持下面的特性:  32地址总线支持最大4GB的物理内存  分段内存模型和平滑内存模型  分页,支持固定大小4KB,提供了虚拟内存管理的方式  支持指令执行(IF、ID、EXEC、MEM、WB)的部分阶段的并行
  Intel 486处理器(1989)
  Intel 486处理器增加了更多并行执行的能力(扩展386处理器的指令译码和执行单元,扩展为5个阶段)。每一个阶段都可以跟其他指令的不同阶段并行执行。除此之外,还增加了以下特性:  8KB的一级缓存,可以增加每一个时钟周期可以执行的指令的数量  X87浮点数  电源管理和系统管理能力  2.2 分段如何产生的
  8086/8088的总线如图所示:
  从图中可知,地址总线是20位。最大可寻址范围  2^20 byte = 1MB  , 而在这个时代寄存器是16位,而IP寄存器的功能是指向下一条要执行的指令的地址。如果要执行指令在内存中的位置超过64KB的地址,IP寄存器怎么去表示呢?要寻址1MB的内存,至少是需要20位的IP寄存器,同理跟IP寄存器关联的其他寄存器都得是20位(比如跟栈帧的创建相关的寄存器:BP,SP都得是20位)。显然是需要对CPU大改了,有没有折中的方案呢?答案是引入段寄存器。
  既然现在已有16位寄存器的成熟技术,在不改原有寄存器的设计的基础上,在CPU中再添加一组寄存器用两个寄存器来寻址即可,但是IP寄存器已有16位,地址总线20位, 只需要利用段寄存器的4位就可以满足需求。
  引入了段寄存器,如何表示20位呢?绝对地址=段基址(段寄存器左移4位) + 偏移地址(IP寄存器的值,offset)
  2.3 段的分类
  了解了分段产生的背景,那么段有几种类型呢?先看下内存的布局
  操作系统肯定也是个程序,程序的指令分布在内存,所有操作系统的指令肯定也在内存。那么问题来了:操作系统和其他程序都在内存。难道其他程序能直接操作操作系统吗?显然不能,操作系统管理了一切,如果可以随便操作,那么计算机将变得非常易碎且非常不安全,所以单独开辟一块内存给OS独享。
  栈和堆都是保存动态数据,分配内存的, 所以从外界看是一块,只要保存两条界限,就不会造成内存溢出,而栈只能向一个方向扩展,所以保存栈底为上界限即可,那么堆就只能选择保存下界限,扩展的方向相反。
  所以段寄存器根据内存布局可以分为:栈段寄存器、数据段寄存器、代码段寄存器和通用段寄存器。
  3. 保护模式
  Intel 386 已经支持了32位的寄存器,最大可寻址就变成了4GB,那么分段是不是就没用了。我们知道CPU控制了整个计算机,操作系统控制了CPU,也就是说,如果不对操作系统进行保护,那么程序只要控制了OS,就等同于控制了整个计算机。而只要切换段寄存器的值就可以达到这个目的。所以必须以某种方式保护OS。最简单的做法就是隔离,将程序和OS进行隔离。
  如何进行保护呢?首先需要解决的问题是不能让段寄存器可以随意切换来实现访问整个内存空间。首先想到的是所有的切换段的操作都必须经过OS来代理。但是这只是软件的处理行为,恶意的开发者仍然可以通过切换段来操作,所以必须通过硬件的配合。
  如果有了硬件的支持,要怎么支持?在切换段的时候,硬件怎么知道哪里是OS哪里是普通的程序?站在硬件角度,多有的软件都是一样的,肯定不区分谁是谁, 唯一能做的是,对软件定义多个类型,硬件只需要识别这个类型。在Intel的CPU其实就是将软件定义了4个级别,而我们熟悉的Linux系统只识别了其中的两个。
  有了特权级的划分,怎么支持切换段,利用原有的段寄存器能否实现切换呢?首先,如果由程序自己完成段的切换,切换之前的段和切换之后的段的程序的级别都需要自己记录,自己记录就有串改的风险,那么需要将这些信息都保存在OS中,只能由OS去完成。而且这些项会比较多,必然需要维护一个表,这些进程和权限一对一的关系,在寄存器中维护显然不可能。所以需要单独开辟内存空间来存储所有的表项-这其实就是GDT(Gloabal Descrption Table)。有了GDT,就不需要段选择器来保存段地址了,因为可以通过查表,要查表就需要记录表项的向量,所以在原有的段选择器中保存这个向量即可,有了这个向量之后就可以通过GDT查表找到对应的下标来找到段地址、特权级等信息来实现切换。问题来了,GDT保存在内存的那个地址呢?引入GDTR寄存器来保存这个值即可。
  根据上述知识的出结论:  程序的特权级保存在哪里?程序执行时,CPU执行的是代码,没有代码,那怎么可能修改数据,所以必然会放在CS寄存器  在访问的时候,需要有一个特权级的校验,这个校验位放在哪里?段描述符
  综上所述,段寄存器中的保存的值做了变更,286和386以后,支持保护模式后,里面存储的值就变成了段选择子了。在设计32位的CPU的时候,已经知道内存的大小是4GB,寻址4GB,必须要使用32位的地址总线和32位的寄存器。这是  i386的设计
  3.1 特权级抽象
  从前文可知,要实现保护模式需要硬件来对特权级提供支持,Intel CPU提供了4种特权级,ring0~ring3表示特权级从高到低,其中ring0通常用于操作系统内核,ring3用于应用程序的特权级。有了这种特权级的支持,怎么在硬件层面上提供保护呢?如果ring3的应用程序去访问ring0的内核空间的数据时,CPU会检测到这种情况,直接抛出GP异常。
  3.2 段描述符表
  从前面的知识可知,需要通过查表来找到段地址信息和权限的校验。这个表就是  GDT  (全局段描述符表)和  LDT  (局部段表述符表)。其中GDT是所有程序和任务共享的,LDT是一个任务独有或者多个任务共享的描述符表。既然 GDT是必选的,也就是说只有GDT就可以支持系统 的所有功能。那么GDT是在哪里呢?从前文可知,GDT的地址是保存在GDTR中,而且GDT是所有段共享的。从前文可知段寄存器位16位,而IP寄存器位32位,所以在32位机中,GDTR只需要48位即可,其中32位指向线性地址,16位指向最大数量(因为段选择器是16位,所以只需要16位即可表示最大范围)。
  那么段描述符表包含哪些内容呢?段所在的地址肯定是需要的,段的范围肯定也需要,除此之外,要完成特权级检测,肯定也需要保存特权级,还有这个段是代码段还是数据段要不要表示,肯定也是需要的。段地址至少需要的32位地址,还有其他的内容需要保存,那么用两个表项来表示一个段是不是就可以。所以一个段描述符用64位来表示。
  段界限(segment limit): 注意段界限的长度位20位,是不是最大就只能表示2^20byte = 1MB, 我们假定它的粒度是byte, 如果粒度是page, 我们知道page = 4KB, 所以最大可以表示2^20 * 4KB = 4GB。 另外一个问题是怎么表示单位呢?其实就是依赖于G标志位,如果G为0表示的是byte, 如果G为1表示的page  基地址:注意有3部分的base,加起来总共是32位,在解析的时候,会将三部分的基地址拼接在一起,形成一个32位的地址。  类型:段有不同的段,数据段,代码段,需要要定义该段的读、写、访问权限。  代码段和数据段描述符类型
  系统段和门描述符类型
  S标志位:系统段是特殊的段,所以需要跟普通的代码段、数据段作区分。这里有点特殊,S标志位为1表示的是代码段和数据段,为0表示的是系统段。  DPL:定义段的权限等级。用于校验对段的访问。所以DPL一定是操作系统维护的,应用程序肯定不能自己去修改,不然就没法校验了。  P标志位:定义段是否存在,如果P标志位设置为0表示不存在,对这个段段访问会产生GP异常  D/B标志位:对32位机,该值位1,16位机该值位0,表示默认的大小或者栈指针大小或者边界大小。  G标志:粒度,字节或者page(4KB)  L标志:表示是64位机的代码。  AVL:保留位,给操作系统软件使用  3.3 段选择子
  内存中的程序有两类,OS和应用程序。在OS和应用程序都是需要执行指令的,都来自于ISA指令流。能不能让APP任意执行OS的指令流,所以需要设置特权级;隔离OS和应用程序。
  CPU控制指令,操作数据的只能指令,只要对指令进行控制,就能控制数据。所有的段的访问,都需要查表(GDT)。 通过GDTR+段寄存器保存的段选择子查找到对应的段描述符。段描述符表查到对应的  段基址  ,通过  段基址 + IP寄存器中的值(段偏移量)= 线性地址  。当前执行指令的代码段在CS 段寄存器中。CS段寄存器保存当前执行指令的代码段,段的权限保存在哪里?段描述符中。
  每次需要用到特权级的时候,都从段寄存器->段描述符走一圈去拿特权级吗?这每次都到内存去取一波,是不是也太慢了,显然不可能,描述符中特权级基本不会变化,而且,段选择子指向的是段描述符表的表象,可以理解为是一对一的关系。所以将段描述符的特权级缓存一份在段选择中,是一个非常合理的设计。而特权级总共4个,只需要2位二进制就可以完全表示。剩下的段选择子的数量肯定不能超过剩下的位数(还有1位标记是GDT还是LDT,所以最大的表象是2^13 个)。CS段寄存保存的特权级可以理解为当前正在执行的指令的特权级,也就是  CPL(current execute pl)  , 很明显,CPL=代码段的特权级。
  对于数据段的访问  ,数据需要指令来操控,没有指令,数据没用。要访问数据,同样要查表(因为需要隔离不同的应用程序)。访问数据,查表,查出来的就是对应的数据段。同CS寄存器一样,DS段寄存器中也需要保存一个特权级,表示当前需要请求数据的权限,即  RPL(Request pl)  。
  每个段寄存器中都需要保存RPL,换个角度,对于CS寄存器而言,他请求的是:CPU去访问某个段的指令,所以它就是RPL,而所有的一切均来源于CPU的指令。所以从这个角度来说:CPL就代表了当前CPU执行指令的PL,即对CS而言CPL就是RPL。SS指向了堆栈段,保存了这些指令流之间的私有数据,所以SS和CS的PL一直,保存的RPL等于CPL。
  CPL是一个特别的RPL,那么有了一个代表应用程序的特权级,那么就需要一个跟他做比较的特权级,这个特权级在哪里?答案是段描述符。因为段寄存器就是个索引下标,通过GDTR查表得到段基址,此时能不能访问,就要看这个段基址能不能被取出来跟IP寄存器一起计算,那么如何做呢?在段描述符中增加一个PL,与请求的RPL来进行比较,由于该特权级保存在段描述符中,所以取名DPL。
  综上所述,Intel CPU 抽象了3种特权级的概念,来完成代码段和数据段之间的特权级校验:  CPL:表示当前正在执行指令的特权级。它本质上是代码段的特权级。正常情况下,CS段寄存器中PL是从对应的代码段描述符中加载而来。  DPL:代表段或者门描述符中定义的特权级级。DPL的解释根据被访问的段或者门的类型的不同会有差异:  数据段、任务门、调用门:表示可以访问该数据段和门的最小的特权级(数值最大)。即如果DPL = 1, 只有CPL = 0或者CPL = 1的程序可以访问。  非一致性代码段:必须是具有相同特权级的代码段才允许调用(CPL=DPL,CPL表示当前执行程序的特权级)。如果是CPL≠DPL,只能通过门来访问。  一致性代码段/通过门调用非一致性代码段:定义了最大特权级(数字最小)。如果DPL是2,0和1是不允许访问的  RPL:请求特权级,覆盖了段选择子的特权级。CPL和RPL共同确定要访问的段是否被允许,校验特权级的时候取的是CPL和RPL的较大值(特权级较小)。RPL存在的目的是避免程序访问到他不想访问的特权级。
  DPL、RPL、CPL之间如何进行比较呢?不管是代码段之间的调用,还是代码段直接操作数据,最终的目的都是对数据进行访问,代码段也是不可写的,所以保护的本质还是保护数据。所以这三者,CPL最大,因为CS和SS代表了当前CPU执行的指令,所以当前执行指令的特权级必定高于对数据的请求,DS等寄存器一定存在关系CPL= DPL的情况是可以调用成功的, 但是非一致性代码段,会改变当前CPL为新代码段的DPL,这时候就可能会出现新的CPL < 原来的CPL, 那么就会出现特权级提升,就有可能访问到特权级比原来调用程序特权级更高的数据段了。这肯定也是不能接受的。所以调用非一致性代码段的规则就简单粗暴,  一定是需要满足CPL = RPL  ;否则处理器会产生GP异常。如图所示:
  代码段C是非一致性代码段。代码段A中的程序  可以调用  代码段C中的程序(使用段选择子C1), 因为C和A的特权级相同  代码段B中的程序  不能调用  代码段C中的程序(使用段选择子C1或者C2),因为两个代码段在不同的特权级不同
  调用非一致性代码段RPL的影响非常有限。因为  一定需要RPL <= CPL, 非一致性代码段程序调用才会成功  。所以上面的例子中, 因为CPL会发生变更,所以调用代码段C,如果成功,会出现CPL = DPL, 所以可以是RPL = 0,RPL=1,RPL=2,但是一定不能是RPL≠3
  指向非一致性代码段的段选择子的RPL的特权级检测的影响有限。RPL的特权级必须小于或者等于调用程序的CPL,只有这样控制转移才会发生。因此,在上面的例子中,段选择子C1和C2只能被设置为0,1,2才是合法的,设置为3是不合法的。  3.4.2 调用一致性代码段
  对一致性代码段的调用,CPL是不会发生变化的,调用程序的特权级不会丢失,在代码段中DPL的含义是可以访问的最小的特权级(数值最小),所以只需要满足  CPL >= DPL  , 否则抛出GP异常。
  在上面的例子中,代码段D是一致性代码段。在代码段A和B中的程序都可以访问代码段D(使用段选择子D1或者D2),因为他们的CPL >= DPL。  对一致性代码段来说,DPL代表的是调用程序可以成功调用的最小的特权级。
  因为调用一致性代码段CPL不会变化,所有有可能出现CPL≠DPL的情况(CPL > DPL), 这种情况的调用不需要发生栈切换。在什么场景需要使用一致性代码段呢?Math函数库和异常处理,这些模块是操作系统内核提供的能力,但是又不需要系统保护,任何代码段都可以访问。但是CPL不会发生变化,所以不会造成调用之后,特权级发生变化,这样解决了访问内核空间的代码导致有权限访问更高特权级数据的情况。
  大多数的代码段都是非一致性代码代码段。  这种代码段的访问,只会是CPL = DPL。要访问CPL≠DPL的代码段,只能通过门的方式才能访问。  3.4.3 通过调用门调用代码段
  调用门的功能是不同特权级的非一致性代码段间的调用,即CPL≠DPL只能通过调用门来调用。除此之外调用门也可以支持16位到32位代码段之间完成调用。从前文可知,代码段访问调用门的规则跟代码段访问数据段的规则一致,即必然存在max(CPL,RPL)<=DPL。
  那么调用门要怎么支持不同特权级代码的调用呢?CS段寄存器只有一个,而特权级会发生变换,必然存在栈的切换,那么代码段寄存器的内容一定是需要切换成新的代码段,这必然导致就代码段的特权级信息的丢失,导致部分特权级提升,那怎么解决呢?引入一个数据结构来保存原来特权级的信息,保存在哪里的呢?能不能保存在应用空间的段呢?显然不能,应用空间可以自己去修改,那必然是要保存在OS中的类似数据段的结构中。那要保存哪些信息呢?  对特权级的处理,降级是不是就可以解决特权级提升的问题。  函数调用需要参数信息,传输传递必然保存在栈中,那么复制几个参数呢?要不要保存,肯定是需要的  此外,调用门类似网关,隐藏了调用代码段的信息。要不要定义代码段的地址,显然可以保存GDT的下标,即段选择子。 要不要定义从代码段的哪里开始执行,定义执行指令的entry point即可
  所以调用门描述符如图所示:
  调用门描述符可能是存在于GDT或者LDT中, 定义了代码段,段选择子,entry point,DPL,拷贝参数的数量。其他逻辑跟数据段描述符类似。
  有调用门就可以完成非一致性代码段,不同特权级的调用。怎么实现的呢?有CPL、调用门RPL、调用门的DPL、目标代码段的DPL可以用于进行特权级的校验。分成两段来看,访问调用门和调用目标代码。
  要访问调用门,一定存在CPL <= RPL, 而访问调用门类似访问数据段,那么DPL代表的是调用门的访问权限,如果max(CPL, RPL) <= DPL, 可以访问该调用门,那么CPL的变更只会减小特权级,不会增强可访问的数据段。如果 max(CPL, RPL) > DPL, 就不可以访问该调用门,直接抛出GP异常。
  通常情况下,非一致性的目标代码段,调用门的DPL = 目标代码段的DPL,所以存在max(CPL, RPL) = 调用门DPL = 目标代码段的DPL, 所以可以完成调用。如果是一致性代码段门外面没有特权级的提升,那么一定存在RPL >= 目标代码段的DPL, 那么CPL的变更是特权级变得更小。所以一定不会出现访问到更高特权级的数据。
  调用门描述符的DPL字段定义了可以访问门的调用程序的最小特权级。如图所示:
  在上面的例子中,在代码段C程序使用段选择子B1或者B2可以访问调用门B,但是不能使用B3来访问调用门B。
  如果一个调用发生在一个更高特权级的非一致性代码段中时,CPL < 目标代码段的DPL,栈切换就产生了。如果调用或者跳转发生在一致性代码段,CPL不会改变,不会发生栈切换。
  那么怎么通过门完成目标代码段指令的调用的呢?
  其实只需要段选择子就可以了,但是为了兼容CALL和JMP指令的far call的调用需要提供CS:IP的方式,需要提供段选择子和段偏移,但是段偏是无用的。如图所示:
  当CPU访问到调用门的时候,它会使用调用门的段选择子来定位目标代码段的段描述符(可能是GDT或者LDT)。然后从代码段描述符中提取基地址,再加调用门的偏移量(offset)就形成了处理程序的在代码段中entry point的线性地址。就完成了跳转到目标代码段执行。  3.5 访问数据段
  对于数据段的访问  ,数据需要指令来操控,没有指令,数据没用。要访问数据,同样要查表(因为需要隔离不同的应用程序)。访问数据,查表,查出来的就是对应的数据段。同CS寄存器一样,DS段寄存器中也需要保存一个特权级,表示当前需要请求数据的权限,即  RPL(Request pl)  。
  CPU只能执行指令,指令才能操作数据,对数据进行读写。那所有指令都能操作所有数据吗?那不就是随意串改了吗?显然是需要对高特权级数据进行保护。怎么保护?只要保证低特权级指令不能操作高特权级的数据是不是就可以了,即max(CPL, RPL) <= DPL。如果不满足该条件会抛出GP异常。
  数据保存在哪里呢?栈和堆。这只是抽象概念。数据可以通过堆栈段寄存器和数据段寄存器来操作。所以要访问数据段中的操作数,必须加载数据段选择字或者栈段选择子(DS和SS)。此外ES,FS,GS是通用段寄存器。
  如图所示的4段程序,分别在代码段A,B,C,D,运行在不同的特权级,想要访问相同的数据段E。  代码段A中的程序使用选择子E1可以访问数据段E,因为CPL = RPL = DPL。  代码段B中的程序使用选择子E1和E2都可以访问数据段E,因为 CPL <= RPL = DPL。  代码段C使用段选择子E3是不能访问代码段E的,因为CPL = RPL > DPL , 使用段选择字E1和E2也是不能访问代码段E, 因为CPL < DPL。  代码段D使用段选择子E3不能访问数据段E,虽然CPL < DPL,但是RPL > DPL。使用段选择子E1和E2可以访问代码段E,因为RPL < RPL。
  从前面的例子得出一个结论,程序或者任务可以寻址的范围会随着CPL的变化而变化。当CPL等于0,可以寻址所有特权级的数据段;当CPL等于1,只可以寻址特权级在1到3这个范围内的数据段,当CPL等于3的时候,只能寻址特权级为3的数据段。段选择子的RPL可以改变程序或者任务可寻址的范围。RPL的正确使用可以防止较低特权程序或过程意外使用段选择子而引起的问题。虽然应用程序可以修改RPL(例如, CPL = 3, RPL = 0),但是这时候CPL会被检查,取的是max(CPL,RPL)。这种机制可以避免故意串改RPL来违背特权级的情况产生。  3.6 访问栈段
  栈是由指令创建的,用于存储指令段产生的局部数据,所以栈段的特权级必然等当前代码段的特权级。所以对栈段的访问,必须满足CPL = RPL = DPL。
  那么指令是如何创建栈的呢?从前面的知识可知, 栈至少需要一个栈顶和一个栈底,即需要SP和BP寄存器的协助才能完成栈的创建。而一个CPU同一时刻只会执行一条指令,所以只会有一个栈处于活跃状态。栈的布局如图所示:
  栈在内存中的布局是一个连续的数组。栈是分布在栈段,由GDT和段选择子可以定位栈。当使用平坦模型的时候,栈可以分布在线性地址空间的任意一个位置。栈的最大大小为4GB(段的最大值,32位的指令地址寄存器)。对栈的操作通过PUSH指令和POP指令就可以。因为栈是向下扩展,堆是向上扩展,所以当push指令执行的时候,需要将SP减少一个单位,栈顶的数据被删除,同理POP指令的执行需要向上增加一个单位,数据放在了栈顶。程序的执行可以创建多个栈,系统中可以创建栈的数量受限于可以创建的段的数量和物理内存的大小。虽然在系统中虽然可以创建多个栈,但是只有一个栈是可用的。当前可用的栈是在SS寄存器引用的栈。
  所以创建栈的过程就需要下面的过程:  创建一个栈段  加载当前栈段的段选择子到SS寄存器中(可以使用MOV,POP,或者LSS指令来实现)  加载栈指针到SP寄存器(使用MOV,POP,或者LSS指令, LSS也可以用于加载SS和ESO寄存器)
  指令只会在一个特权级下执行吗?显然不是,用户空间到内核空间必然会出现特权级的变化。特权级变化后,还能用当前的栈吗?如果是,遍历一下,就实现了访问高特权级的数据了,显然不同特权级需要不同的栈。  那如何完成不同特权级栈的切换呢?
  前面提到,段内的指令片段间的调用需要完成栈帧的创建。那么如果是不同far call呢?仅仅创建栈帧能实现吗?far call的调用可能特权级都不一样,有可能调用一个更高特权级的代码(DPL < CPL),如果在同一个栈段中,那么直接遍历栈是不是可以拿到高特权级的数据。显然是不行的。要解决这个问题,在进程运行过程,肯定需要为每个特权级创建栈,也就是最多4个栈。因为Intel CPU定义可4个特权级(ring0~ring4)。显然这四个栈有不同的特权级,特权级最终定义在GDT中,GDT中的每一个描述符表只有一个DPL字段,段基地址定义了段的线性地址。所以这4个栈位于不同的段中。栈段选择子指向的是段,所以每个栈指向不同的段,而且同时只有一个栈段处于活动状态。
  从上一节可知,栈的切换是需要恢复啥信息,就需要将啥信息保存。所以当ring3的代码段调用ring0的代码段时,SS、SP、CS、IP都需要保存在栈中。而在任务切换的过程,实际是不允许高特权级直接调用低特权级代码段的,所以TSS中不需要保存特权级为3的栈。只有特权级ring0,ring1,ring2的栈有可能被低特权级调用,所以TSS只需要保存3个栈的引用。
  调用门的参数数量字段定义了处理器应当从调用栈中拷贝的参数,上限是31.如果超过31个参数需要被传输,可以使用一个参数,引用一个数据结构,或者保存SS和ESP寄存器的内存可以被用来进行参数的传输。  3.7 访问指令段内嵌数据
  代码段是只读的,所以代码段内前的数据结构必然是只读的。所以有以下3种情况:  访问的数据位于当前执行代码同一个段中,那么必然存在CPL = DPL。  访问的数据位于非一致性代码段中,跟数据段访问规则一致,即CPL = DPL, 或者通过门访问,CPL <= DPL。  访问的数据位于一致性代码段中,那么CPL <= DPL。  3.8 小节
  Intel CPU在分页模式和分段模式下都是支持保护模式的。保护模式的实现需要依赖于硬件的支持,提供了对段和页提供访问限制的能力。总共支持4种特权级,ring0~ring4。通常的应用场景,应用程序的特权级比操作系统的特权级更低,通过保护模式,可以避免较低特权的应用程序访问到更高特权级的代码和数据。这增强了应用程序和操作系统的安全性和鲁棒性。
  当启用保护模式后,对内存的访问都会进行保护模式的校验。所有的检测都会在真正开始操作内存之前进行,违背保护模式的操作都会导致GP异常的产生。
  分段模式下的保护模式:CR0寄存器的PE标志位的设置可以切换到保护模式。一旦被设置了,就不能再进行保护模式的启用和禁用。通过将所有的段描述符号和段选择子的特权级设置为0(最高特权级),可以实现在保护模式下禁用部分保护模式的检测。
  保护模式的实现,依赖于段描述符,主要有两种类型的段描述符,系统段描述符和非系统段描述符(代码段和数据段描述符),如图所示:

天气大转折!今天下午到明天我省将迎大范围降雨今天一大早就热得不得了早上7点河南霸榜全国气温前十除了西部山区其它地区大都在30上下闷热感扑面而来!象粉儿们也都在追问雨的消息来,让我们一起看看6月下旬雨的行程第一轮2223日强降酒桌上,当对方有这5个举动的时候,其实是在赶你走,别装不懂酒桌上,就是人情世故编织的小世界。端起酒杯的时候,有的人看起来毕恭毕敬,实则是面谀背毁有的人看似波澜不惊,实则内心早已经惊涛骇浪有的人乍一看满脸堆笑好接近,实则是笑里藏刀心怀鬼胎酒江苏女神学霸学分绩点4。0,获省教育厅表彰,已收到国企offer每一个闪闪发光的人背后,都有晶莹汗水的浇灌每一条通往成功的道路上,都需要不改初心的坚持。大学四年,学校给予学子打开一扇又一扇大门的勇气,学子则回馈以一步又一步的前进。今天,就让我们胃癌早期不痛不痒?饭后出现4个异常,多留意,或是胃癌来临王伯上个月陪妻子刘姐去医院做胃肠镜检查,看到妻子做无痛胃镜肠镜检查时,只是睡了几分钟检查就完成了,为求心安,王伯也给自己预约了一次无痛胃镜检查。经过仔细检查,医生发现他的胃窦部有一人红是非多?刘亦菲14岁入学北电再起争议真的是人红是非多么?反正刘亦菲又双叒叕被狙了。不是演技,不是颜值,而是关于20年前她以14岁的年龄入学北电的争议。那我实话说一句,14岁上大学是真的有够夸张的哈,搜了一下网上关于这全球第一强国美国病得不轻,可药在哪里?美国,这个曾经代表整个人类文明灯塔和希望的国度,如今正在经历一场前所未有的危机。从经济社会到整个国家生活,美国的方方面面都在偏离正常轨道。美国现任总统拜登在上周四接受美联社采访时忧这么多待播剧有你的菜吗?第七部我超期待最近剧荒,真的不知道看什么,各大视频网站晃,没有几部看得进去的,这不,就看到这些剧的海报了!杨幂许凯的爱的二八定律我有点期待,听说是姐弟恋,应该挺甜。隔壁杨颖和赖冠霖正在爱情应该有土耳其航空将订购30架支线飞机中国航空新闻网讯据简单飞行网站报道,随着旅行限制的逐渐放宽,航空旅行的需求也在飞速上涨,土耳其航空公司将订购30架支线飞机以缓解压力。据报道,早在2019年,土耳其航空当时的董事长幼小衔接涛声依旧的来源竟是一首诗?诗句赏析古古识字枫桥夜泊这首诗将作者羁旅之思,家国之忧,以及身处乱世尚无归宿的顾虑充分地表现出来,是写愁的代表作。月亮已经落下,乌鸦啼叫寒气满天。江边的枫叶,渔船的灯火,都抵消不了王冰冰重返大学拍短片,身穿校服的甜美样子既像吴昕也像谭松韵王冰冰重返大学拍短片,身穿校服的甜美样子既像吴昕也像谭松韵今日头条晓今娱立足娱乐圈,聚焦正能量!本文由晓今娱原创,欢迎关注,带你一起长知识!毕业季之际,王冰冰重返大学校园出演校园大教育部直属大学新排名,北京大学拔得头筹,复旦大学令人叹息新的一年的高考结束了,广大高三学子们那一颗紧张担忧的心总算是可以放下了,心里积压了三年的大石头也算是落地了。十几年的寒窗苦读,拼的就是这短短几天的答卷,为的就是日后能够考上一所好大
西媒美联储把美国通胀负担转嫁给他国参考消息网11月23日报道西班牙世界秩序网站11月21日刊登题为美国向世界输出通胀来挽救自身经济的文章,作者是西班牙卡米洛何塞塞拉大学研究员胡安巴斯克斯。全文摘编如下美联储正在把美拯救推特靠裁员,提振特斯拉靠什么?头图来源社交媒体美国时间11月21日,推特的裁员大调整终于结束了。刚刚入主的马斯克用了三周时间,将推特原有的7500人团队规模调整到如今的2700人。此时,世界杯已经进入了第二个比单月销量突破五万!比亚迪宋PLUS有没有可能创造月销10万的神话?在国人心目中,新能源汽车的发展还是不够成熟,相较燃油车的多年沉淀来说,纯电动太年轻了,伴随的很多问题都难以解决,其中巡航限制就是比较重要的一个,2022年5月,比亚迪推出了宋PLU主流大厂全面进击新能源市场,上汽通用700亿加速电动化智能化转型曾经风光无限的造车新势力们近两年遭遇了来势汹汹的寒潮。先有拜腾赛麟汉腾等破产出局,今年10月,小鹏汽车销量接近腰斩,蔚来零跑等头部品牌也大幅下滑。新势力的集体示弱,让人们的目光又重三十四十岁以上的姐妹看过来,实打实好的护肤好品想做一篇针对成熟肌护肤的良心护肤品小合集,大牌到国产小众都有,每一款都有真实的推荐理由,咱们用数据说话,实践才是检验真理的唯一标准嘛,话不多说,继续看下去吧。海蓝之谜修护精粹水参考刘强东对得起兄弟!为德邦快递员缴纳五险一金,美团真该学一学寒冬还在继续?京东也举步维艰,刘强东不得不做出重要决定!春公子之前就说过,京东创始人刘强东是非常值得年轻人们学习的一位企业家!刘强东出身草根,但刘强东并没有怨天尤人!而是凭借自身的建筑行业真的是夕阳产业了吗?一行业分析不在建筑行业或没有建筑行业的熟人,肯定不知道近几年房地产的风雨飘摇,以房地产为国家支柱产业的黄金十年已经过去,最近几年国家陆续出台了不同的限购措施,房产税也已蓄势待发,2黑马冲击科创板,蜂巢能源搅动动力电池行业格局文江南当行业步入太瓦(TWH)时代,动力电池的竞争格局却还未尘埃落定。追赶者们高歌猛进,冲击着宁德时代比亚迪们的霸主地位。蜂巢能源即是追赶者中的一员。这家2018年2月创立的动力电凭任何一点,刘强东就是我最佩服的良心企业家!没有之一,信不?东哥又上热搜啦!因为,刘强东要对员工和高管待遇进行一升一降他要给2000多名高管降薪,相反要提高基层员工的福利待遇。他承诺,京东集团出100亿设立基层员工住房保障基金,而他个人自掏林蔚小米11系列售后新政基本覆盖产品生命周期,比较合理近日,小米发布了新公告,公布了一系列关于小米11系列手机的售后服务新政,其主要内容为小米11系列手机在保障三包基础上,36个月保修期免费维修,不含人为损坏及非主板质量问题。这个政策云南8个村落入选这份全国名单!有你的家乡吗?来源云南日报近日农业农村部办公厅公布了2022年中国美丽休闲乡村名单推介255个乡村为2022年中国美丽休闲乡村其中云南有8个乡村入选名单2022年中国美丽休闲乡村名单(云南省)(