什么是保护模式
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 = 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 = 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(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 用什么权限去访问一个段