深耕保护模式

 

什么是保护模式

X86 CPU的3个工作模式:实模式、保护模式和虚拟8086模式。
现在的操作系统都是基于保护模式。
保护模式的两个重要机制:段的机制、页的机制。

 

段的机制

段的机制非常复杂,了解段机制就要先了解段寄存器。

段寄存器

汇编读写一个地址:

mov dword ptr ds:[0x123456],eax

读写的地址实际上是ds.Base+0x123456
段寄存器一共有八个:

ES CS SS DS FS GS LDTR TR

段寄存器的结构(一共96位):

只有16位的可见部分:Selecter。

可以通过结构体表示为如下:

struct SegMent
{
    WORD Selecter; //16位的可见部分
    WORD Atrributes; //16位的属性
    DWORD Base;     //32位的Base(从什么地方开始)
    DWORD Limit;    //32位的Limit(长度)
}

段寄存器的读取:

mov ax,ds  //只能读16位可见部分

段寄存器的写入:

mov ds,ax //写入是96位

段寄存器属性:

段描述符

这里涉及到两张表:GDT(全局描述符表)与 LDT(局部描述符表)。windows用的是GDT表,LDT表几乎没用。

当我们执行类似MOV DS,AX指令时,CPU会查表,根据AX的值来决定查找GDT还是LDT,查找表的什么位置,查出多少数据。

gdtr是一个寄存器,里面存的是gdt表的开始位置和大小。

查看gdt表开始位置:

r gdtr

查看gdt表的大小:

r gdtl

查看某个位置的数据(四个字节一组):

dd 8003f000

gdt中存储的数据是段描述符,八个字节一组。

八个字节一组查询:

dq 8003f000

要想查询到更多的数据,命令:

dq 8003f000 L40

L后面是多少组。

段描述符结构:

前面四个字节(高八位)是图上面32位,后面四个字节(低八位)是图下面四个字节。

段选择子

段选择子是一个16位数,该数决定了取gdt表中查哪一个数据。这个值就是MOV DS,AX给的AX的值,从这个值中取赋值剩下的80位(96-16)。

  • RPL:请求特权级别。
  • TI:TI=0 查GDT表,TI=1 查LDT表。
  • Index:处理器将索引值乘以8在加上GDT或者LDT的基地址,就是要加载的段描述符。

加载段描述符至段寄存器(其他几个汇编指令修改段寄存器)

除了MOV指令,我们还可以使用LES、LSS、LDS、LFS、LGS指令修改寄存器。

CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CS与EIP一起改。

char buffer[6];                    
__asm                            
{            
    les ecx,fword ptr ds:[buffer] //高2个字节给es,低四个字节给ecx    
}

注意:RPL<=DPL(在数值上)

不会出现访问错误,可以运行的:

char buffer[6] = {0x00,0x26,0x00,0x00,0x00,0x00};
_asm 
{
    les ecx,fword ptr ds:[buffer]
}

段描述符属性——P位和G位

P位

  • P = 1 段描述符有效
  • P = 0 段描述符无效

通过指令将段描述符加载至段寄存器中,先检查P位,如果为0,那么该段描述符无效,其他检查都不做了。

段描述符与段寄存器的对应关系

  • WORD Selector; //16位 对应段选择子,是mov ds,ax中的ax。
  • WORD Atrribute; //16位 对应高四字节,第8位到23位
  • DWORD Base; //32位 比较碎,由三部分组成:高四字节的24到31位,高四字节的0到7位和低四字节的16到31位
  • DWORD Limit; //32位 高四字节的16到19位和低四字节的0到15位,这个Limit是最多20位,显然不是32位,实际上和G位有关。

G位

当G = 0 时,Limit的单位是字节,Limit界限最大为000FFFFF。

当G = 1时,Limit的单位是4kb,4kb实际上是4096个字节。地址是从0开始的,所以是4095,对应16进制为FFF,所以相当于Limit算成最大值是FFFFF(字节),他的单位是FFF(字节),实际上是FFFFFFFF,刚好8个F。

段描述符属性——S位 TYPE域

S位:

当S位为1时,是一个代码段的描述符,或者是一个数据段的描述符。

当S位为0时,是一个系统段的描述符。

S位决定着type域的含义。

如何直观的看出哪些是代码段或者数据段?

观察这四位。

  • 先看P位,P是代表着该段描述符是否有效,那么要想有效就必须是1。
  • 再看S位,S要想代表数据段或者代码段也必须为1。
  • 最后看DPL,DPL这两位只能是同为1或者同为0。

那么这四位如果转化成16进制,只能是9或者f。再看gdt表:

直接看高四字节的第五位是否是9或者f就行了,只有这两个数,这个段才有可能是数据段或者代码段。

当s=1时,如何区分是代码段还是数据段呢?

观察type域表:

  • 当第11位为0的时候,该段为数据段。
  • 当第11位为1的时候,该段为代码段。

所以还是观察gdt表,当高四字节的第6位大于或者等于8的时候,为代码段。当高四字节的第6位小于8的时候,为数据段。

数据段描述符说明

当该段为数据段时,也就是11位为0时。

type域除了第11位以外还有三位。

分别表示该段属性:

  • A 访问位,表示该位最后一次被操作系统清零后,该段是否被访问过.每当处理器将该段选择符置入某个段寄存器时,就将该位置1.
  • W 是否可写 可写:W=1
  • E 扩展方向

关于扩展方向,当E=0时,向上扩展。当E=1时,向下扩展。

向上扩展时,段的有效范围是fs.Base+Limit。(图左)

向下扩展时,段的有效范围是除了fs.Base+LImit的范围。(图右)

代码段描述符说明

当该段为代码段时,也就是11位为1时。

  • A 访问位
  • R 可读位
  • C 一致位

C = 1 一致代码段

C = 0 非一致代码段

(关于什么是一致代码段和非一致代码段参见代码间的跳转 )

系统段描述符说明

当S=0时,该段描述符为系统描述符.系统描述符有分为以下类型:

这里没有什么好区别的,主要是知道对应的含义,这里和什么调用门,中断门有关,后面再讲。

描述符属性——DB位

DB位对下列三种情况有影响:

情况一:对CS段的影响。

  • D = 1 采用32位寻址方式
  • D = 0 采用16位寻址方式

前缀67 改变寻址方式

情况二:对SS段的影响。

隐式对战访问指令是并不直接修改esp的值,通过压栈出栈等方式间接改变ESP的值。

  • D = 1 隐式堆栈访问指令(如:PUSH POP CALL) 使用32位堆栈指针寄存器ESP
  • D = 0 隐式堆栈访问指令(如:PUSH POP CALL) 使用16位堆栈指针寄存器SP

情况三:向下拓展的数据段。

  • D = 1 段上线为4GB
  • D = 0 段上线为64KB

段权限检查

CPU权限等级划分

如何查看程序处于几环?(CPL)

CPL(Current Privilege Level) :当前特权级

CS和SS中存储的段选择子后2位.

比如拖入一个三环程序到OD,cs的段选择子实际上是0x001B,转化为2进制最后两位(CPL)是11,那么就是3,表明是一个三环程序。

通过windbg下断点去看cs段寄存器,发现他的段选择子是0x0008,转化为2进制最后两位(CPL)是00,那么就是0,表明是0环程序。

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

DPL存储在段描述符中,规定了访问该段所需要的特权级别是什么.

通俗的理解:

如果你想访问我,那么你应该具备什么特权。

举例说明:

mov DS,AX

如果AX指向的段DPL = 0 但当前程序的CPL = 3 这行指令是不会成功的!

RPL(Request Privilege Level) 请求特权级别

RPL是针对段选择子而言的,每个段的选择子都有自己的RPL。

举例说明:

Mov ax,0x0008 与 Mov ax,0x000B //段选择子

Mov ds,ax Mov ds,ax //将段描述

指向的是同一个段描述符,但RPL是不一样的

数据段权限检查举例

参考如下代码:

比如当前程序处于0环,也就是说CPL=0

Mov ax,000B //1011 RPL = 3

Mov ds,ax //ax指向的段描述符的DPL = 0

数据段的权限检查:

CPL <= DPL 并且 RPL <= DPL (数值上的比较)

注意:

代码段和系统段描述符中的检查方式并不一样,具体参考后面课程.

 

总结

CPL CPU当前的权限级别

DPL 如果你想访问我,你应该具备什么样的权限

RPL 用什么权限去访问一个段

(完)