x86特权级总结

特权级变换时栈如何变化

cpu从低特权级代码段转移到高特权级代码段 栈从tss中读取到,

从高特权级代码段转移到低特权级代码段, 低特权级的栈指针可以从当前栈底部拿到(低->高转移时保存的)

CPL和DPL

对于受访问为数据段来说:

只要CPL<=DPL就可以访问到此数据段的内容

对于受访问为代码段来说:

只有相同特权级别才能访问,(高特权级代码段->低特权级代码段只有中断能做到,低->高从数据段的角度来说都不行)

如果当前cpl=2,只能转移到dpl=2的代码段上去执行

唯一一种处理器从 高特权级降低到低特权级运行的情况:

处理器从中断处理程序中返回到用户态的时候

,高特权级代码段能做到 低特权级所有能做到的,所以没有必要

除了中断返回,否则不会从高特权级的代码段返回到低特权级的代码段

中断处理程序中有某些特权指令需要执行,所以中断只能在0特权级别


cpu如果仅仅只能平权访问代码段的话,其它特权级代码段访问不了,那么没意义,所以提供了 手段 来转移到高特权级代码

另一种好办法是: 既执行高特权级代码段上的指令,又不提升特权级

利用一致性代码段,

描述符为非系统段(存储段 S=0)时,type字段中 Cbit 表示代码段是否有一致性,

非一致性代码段只能平级转移

一致性代码段\依从代码段 检查过程

从低特权级代码段转移到依从代码段,要求自己的特权级要比转移前的CPL高,

数值上: DPL<=CPL

这也表示了依从代码段的DPL就是 权限的上限

在此上限之下的特权级代码段都可以转移到此代码段中执行

特点是: 转移后的特权级仍保持不变

4种门结构

都用来实现从低特权级别向高特权级别转移

  1. 任务门:

    任务以任务状态段TSS为单位,实现任务切换,可以借助中断发起(在IDT中),在中断发起时,如果对应中断向量号是任务门,则发起任务切换

    也可像调用门一样,call jmp指令后接任务门选择子或tss的选择子

  2. 中断门:

    以int指令主动发起中断的形式从低特权级向高特权级转移

  3. 陷阱门:

    以int3指令主动发起中断的形式, 一般编译器用

  4. 调用门:

    call和jmp指令后接门选择子为参数,(可用来实现系统调用),call指令使用调用门可以提权转移,而jmp指令使用调用门只能向平级代码转移

任务门可以放在GDT,LDT,IDT中

调用门可以放在GDT,LDT中

中断门,陷阱门只能放在IDT中

任务门和调用门可以直接用call和jmp指令调用,后面接描述符的选择子就行

陷阱门和中断门只存在与IDT中,不能主动调用,只能由中断信号来调用触发

因为这些描述符中可以确定具体地址,所以调用指令的偏移量会被忽略


调用门特权级检查过程

门有门槛和门框,

门槛: 是访问者特权级的下限,访问者特权级必须大于门描述符的DPL,否则连门都进不去,数值上: CPL<=门的DPL, 此时门描述符相当于数据段描述符一样,只能允许比自己特权级高或者相同特权级的程序访问

门框: 是访问者特权级的上限,访问者特权级再高也不能比描述符中目标程序所在代码段的DPL高,否则本身特权级比目标代码特权级高,那怎么还是向高特权级转移呢???,

所以 门中目标程序所在段的特权级DPL <= CPL

最后,进门之后,处理器将以目标代码段DPL作为当前特权级CPL

cpu除了检查DPL和CPL,参与的还有RPL

[查看下面的调用门特权检查过程]

调用门的工作流程

已知调用门描述符存的是程序代码段选择子+程序在代码段内的偏移,

所以call调用门首先在GDT/LDT中找到门描述符,然后根据描述符中的目标代码段选择子来找到目标代码段的描述符,从此描述符中拿到目标代码的基地址,加上门描述符中的偏移地址就得到了调用门对应的例程

(未开启分页)

通过调用门调用例程,参数如何传递?

当通过调用门从特权级3转移到特权级0时,栈从3切换到0,

在call调用门前,参数已经在3特权级的栈中准备好了,当转移到0特权级时,cpu会将参数自动复制到0特权级的栈中

注意调用门的描述符高32bit中 参数个数 选项,5 bit,最多复制(传递)2^5-1=31个参数

调用门的使用

例如:call selector_gate:offset,因为描述符中已经有偏移地址,所以offset会被忽略

jmp 调用门不能回头,

call 调用门,cs和eip的值在栈中,可以通过retf返回

调用门的过程保护

调用的过程

  1. 提供调用门函数例程的参数入栈
  2. 根据call/jmp调用的”调用门”描述符确定转移后的CPL,根据CPL判断是否需要切换栈,从TSS中找到ss:esp
  3. 检查ss的属性
  4. cpu先临时保存原栈的ss和esp,->将新ss:esp加载到寄存器中->将临时保存的old ss 和 esp入新栈
  5. 将参数从原来的栈复制到新栈中,(个数由描述符来决定)
  6. cs选择子要重新加载,(那么 段描述符缓冲寄存器会被刷新),将当前cs:eip压入新栈
  7. 将目标代码段的选择子和eip加载到寄存器中

调用门如何返回?(retf指令从调用门返回过程

目前经过调用门后的栈(假设有2个参数)的情况:

32bit宽度
ss 栈底部(高地址)
esp
参数1
参数2
cs
eip <—–ss:esp (低地址)

cpu只直到执行指令,并不知道现在刚刚从低特权级通过了调用门过来的,所以我们要给cpu发出指令人为的回到低特权级

retf指令返回过程:

  1. 检查特权级,根据栈中cs的选择子的RPL和选择子指向的描述符的DPL来判断是否要改变特权级,
  2. 检查通过,则将栈中cs和eip出栈恢复寄存器的值
  3. retf后的参数=参数个数*参数大小,根据此参数的值增加栈指针esp,目的是跳过从低特权级栈复制到高特权级栈中的参数( 我们直到传递了几个参数)
  4. 将当前esp处的原来的esp和ss恢复到寄存器中,于是恢复了旧栈.

回到了低特权级程序中

调用门的例程使用了自己的数据段怎么办?

接上

当通过调用门后进入内核代码(高特权级代码,CPL=0),内核会使用自己的数据段,则数据段描述符的DPL=0,那么此时数据段选择子ds.RPL必须=0才能够访问到内核数据段(描述符),

从此之后,ds寄存器一直都是指向内核数据段的选择子,当调用reft返回到低特权级代码后,cpu只恢复了ss,esp,cs,eip寄存器,像ds,fs,gs等寄存器不会被还原

此时在低特权级仍然可以直接访问到内核数据段 ((( !!!注意,尽管当前特权级不够,但是特权级检查只发生在 像寄存器中加载选择子的时候 )))

解决方法:

  1. 人为在调用门例程中的内核代码中 使用数据段寄存器,将其入栈,然后更新选择子,退出前恢复就行
  2. Linux不使用调用门,用中断门来完成系统调用

以上是通过代码解决,但CPU解决了,并没有交给软件层来解决,

在返回时,如果改变了特权级,CPU会检查所有的数据段选择子,若DS,ES,FS,GS中的选择子指向的描述符的DPL高于当前特权级(CS.RPL),会将此寄存器全填充0,也就是指向GDT的第0个

并没解决问题,但是再次访问数据会引发异常(GDT表中第0项为空)

RPL

为什么需要RPL?

例如调用门提供一个从硬盘读取扇区到指定内存地址的例程,接收参数(硬盘LBA,数据段选择子,段偏移地址)

一般情况下我们会老实给出特权级3的数据段,但是如果参数给的是内核的数据段怎么办??

在执行例程时(只检查CPL和DPL),此时CPL=0可以通过任何检查,那么就会破坏内核的数据,

传递内核数据段选择子,调用门例程以为请求者是操作系统(因为选择子对应的描述符的DPL=0),但是实际的请求者是用户(3),

RPL的作用就是代表真正的访问者的身份

当用户使用系统调用陷入内核,选择子是由OS构建,并且由OS提供,当用户提交自己的选择子时,OS会将用户提交选择子的RPL修改为3

RPL的目的是: 避免低特权级程序访问高特权级的资源,

用来检查当前请求者和真正的资源需求方是否都具有访问受访者的资格,原因是请求者和资源需求方不是一个人,请求者可能是代理人

特权级要求

例程在访问数据时会进行特权级检查:需满足如下条件

CPL<=DPL,RPL<=DPL


即使用户提交的是内核选择子,RPL=0,但是OS直到是用户进程的参数,会将RPL修改为3

那么执行例程时: CPL=0,RPL=3,数据段描述符的DPL=0

CPL<=DPL,RPL<=DPL不满足,


修改RPL指令

arpl 通用寄存器/16bit内存,16bit通用寄存器(cs寄存器的值)

rpl都被会置为cs.rpl的值

直接访问一般数据和代码时的特权检查规则

代码段

  1. 受访者为 非一致性代码段:

    CPL=DPL=目标代码段DPL

  2. 受访者为 一致性代码段

    CPL>=DPL && RPL>=DPL (低特权级不提权执访问高特权级代码)

特权检查发生时间:改变cs,eip的指令,(既发生特权级转移时),如:call,jmp,int,ret,sysexit

数据段

  1. 受访者为数据段

    CPL <= DPL && RPL <= DPL

  2. 栈段的特权级检查:

    栈的特权级=CPL,(各个特权级有相应的栈),修改ss时,选择子对应的描述符的DPL要等于当前CPL,

    既CPL=RPL=ss对应的描述符的DPL

特权级检查发生在: 向ds,es,gs,fs中加载选择子时


调用门特权检查过程

出现的特权级:调用门的选择子RPL_GATE,调用门描述符中的选择子RPL_CODE

                    调用门描述符的DPL_GATE,调用门描述符中的选择子对应的代码段描述符DPL_CODE
  1. 上面提到过门框和门槛,要求 DPL_CODE <= CPL <= DPL_GATE

  2. RPL_GATE <= DPL_GATE:

    1. 为什么RPL_CODE不参加特权级检查?

    因为门描述符中的选择子不代表真正的请求者,真正的请求者是RPL_GATE

    1. 为什么RPL_GATE只和DPL_GATE比较?

    因为RPL_GATE(选择子)只用来索引调用门,不用来引用内存,过了调用门后不需要了


调用门是通过call和jmp调用的,

当调用门对应的代码段是一致性代码段时,jmp 和 call都是上面的检查规则

当对应的代码段是非一致性代码段时,call和上面检查规则一样,

因为jmp只能平级跳转,所以用jmp指令调用 “调用门“的规则是:

  1. DPL_CODE = CPL <= DPL_GATE
  2. RPL_GATE <= DPL_GATE

IO 特权级

特权级不仅体现在数据和代码中,特权级也体现在指令中

  1. 对计算机执行有严重影响的指令:

    只有在0特权级才能执行的指令称为”特权指令(Privilege Instruction)”,如 hlt,ltr,lidt

  2. 体现在IO读写上,eflagsIOPLbit和TSS中的IO bit map决定执行IO操作的最小特权级,

    IO相关指令只有在CPL <= IOPL才能执行,称为IO敏感指令

    权限不够,触发CPU异常,如:in,out,cli,sti

IOPL bit

eflags中12-13bit是IOPL位(I/O privilege level),

作用:

  1. 限制当前任务进行IO的敏感指令的最低特权级,
  2. 用来决定任务是否允许操作所有的IO端口,IOPL是打开所有IO端口的开关

每个任务(内核 或 用户进程)都有自己的eflags寄存器的值,所以每个任务都有自己的IOPL.

当任务的当前特权级 CPL <= IOPL 则允许执行全部IO指令

如何修改eflags中的IOPL

  1. pushf / popf
  2. iretf

TSS的IO bit map

当CPL <= IOPL,可以访问64k个端口,

当CPL > IOPL,并不是不能进行任何IO操作,可通过io bit map设置部分端口的访问权限 (整体关闭,局部打开)

位图中的每一个bit代表一个端口,0表示相应端口可以访问,1不能访问,(bit map只有在CPL > IOPL才有效)

bit map在TSS中既可以存在,也可以不存在,不存在表示禁止访问所有的端口,tss不包括bit map的大小是104字节

如果CPL > IOPL并且根据Tss界限得知bit map不存在则cpu异常

为什么IO bit map结尾是0xff

IO端口按照字节编址,一个端口只能读写1个字节的数据,如果对一个端口连续读取多个字节,是以该端口号为起始的多个端口一并读入

in ax,0x234 ;从0x234端口读取2byte数据

等价与:

1
2
in al,0x234
in ah,0x235

如果0x234端口对应的bit是前一个字节的最后一位,则0x235对应后一个字节的第0bit,则cpu要读取2字节来检查.

如果前一个字节已经是bit map中最后一个字节了,那么下一个bit对应的字节越界了.cpu允许IObit map不映射所有端口,既IO bit map的长度可以<8192byte,

  1. 在范围外的端口,cpu禁止访问,0xff正好表示bit=1禁止访问
  2. 0xff作为bit map的边界标记

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!