这节会从多个方面测试段会进行哪些权限检查,首先CPU可以在4个级别上运行ring 0,ring 1,ring 2,ring 3,运行的级别越低权限越高,我们常常挂在嘴边的内核,就是当CPU运行在Ring 0级别上运行的时候,CPU当前运行级别就是在执行当前指令的时候CS段寄存器或者或者SS段寄存器的选择子16位的低2位值就是当前CPU处理哪个级别,也称为了CPL。
CS寄存器存储的选择子任何时候和SS段寄存器存储的选择子低2为任何时候都是一样的,因为CPU运行在不同的级别,会使用不同的堆栈,所以操作系统准备了4个堆栈,当CPU运行与不同的级别会使用不同的堆栈,所以CS段寄存和SS段寄存器必须是配对的,指令运行的时候需要使用堆栈(函数调用参数等等局部变量的使用),cs寄存器或者ss寄存器的低2位值就是在那一时刻CPU运行的级别,称为cpl,我们平时说的应用程序就是3环程序cpl只能为3,内核ring0 cpl=0,这是本节需要提前知道的只是
本节会给大家验证段机制为了保护内存会进行哪些检查
数据段的段加载权限检查是RPL<=DPL(数据越小权限越大)同时CPL<=DPL请看下面证明
我们GDT表中构建了一个数据段的描述符, 段选择子0x93 RPL=3 段描述符的DPL=3 执行 mov ds,ax cs=0x1b CPl=3 ,此时 RPL=3=DPL=CPL段描述符加载成功
接下来段选择子位0x90 RPL=0 以最高请求权限请求加载这个段描述符,DPL=3,CPL=3 RPL<=RPL CPL<=DPL ok访问成
接下来把0x90选择子索引处的段描述符与修改DPL为2
此时 RPL=0 DPL=2 CPL=3 RPL<=DPL成功但是 CPL<=RPL不成功(段描述符加载失败)
因为在执行mov ds,ax CPL位3 过不了 CPL<=DPL的检查
验证数据段内存访问
构建一个数据段描述符 P=1 G=1 S=1 TYPE=3 DPL=3 TYPE=3 A=1 W=1 可读可写
Base=0x00000000
Limit=0xFFFFFFFF 可以看到 0-0xFFFFFFFF之间的内存可读可写 没有问题
构建一个数据段描述符 P=1 G=1 S=1 TYPE=1 DPL=3 TYPE=3 A=1 W=0 可写
Base=0x00000000
Limit=0xFFFFFFFF 可以看到 0-0xFFFFFFFF
此时RPL=0 DRPL=3 CPL=3 mov ds,0x90这条指令的段检查可以过
但是mov dword ptr ds:[taolaoda],0x666666; 这条指令访问了内存,此时ds段是不可写的,段的访问属性检查不过了
数据段E=0是向上扩展 访问段可访问区间是 Base~Base+Limit
构建一个数据段段描述符 G=0; Base=0000000 Limit=0xfffff DPL=3 选择子位0x90 ,RPL=0
执行mov ds,ax 时 RPL<=RPL CPL=3<=DPL可以段加载权限检查
W=1可读可写,可以过段内存访问检查,此时ds段可以访问的位0~0xFFFFF
taolaoda变量的地址是0x425A30不在0~0xFFFFF段越界了 直接挂,所以在访问内存的时候还会进行一个边界检查
为了使大家对数据段 E=0向上扩展的段的范围是Base~Base+Limit 看的更加直观,构造一个段描述符 数据段的属性位可读可写
VirtualAlloc函数在虚拟地址0x20000000开辟2个页的内存,权限为可读可写可执行
Base=0x00000000
Limit= 0x20000fff ds段能访问的地址是0x00000000-0x20000fff
在访问内存的时时候访问0x20000000虚拟地址的时候写入数据成功,在写入taolaoda全局变量的时候也成功,在访问0x20001000虚拟地址的时候挂了,段溢出了出了异常,所有可以推断,在访问内存的时候,每一条访问内存的指令,段保护机制都会检查,段有没有溢出,有没有访问权限,否则报异常,这就是对段对内存的保护机制
上面同样的0x20001000虚拟地址以其他段的方式进行访问,
0x93选择子的对应的 P=1 G=1 RPL=3 DPL=3 是数据段,可读可写
Base=0x00000000 Limit=0x20000FFF
0x23选择子对应的段 是数据段 P=1 G=1 RPL=3 DPL=3 可读可写
Base=0x0000000 Limit=0xFFFFFFFF
同样的虚拟地址0x20001000以es段访问时可以的,但是如果以ds的方式去访问就挂了
验证P位检查构建一个数据段描述符P=0,这个描述符没有意义的试试在ring3加载会怎么样(把这个放在最后是因为这个很容易被忽略)
总结数据段的检查有4种
1.加载段描述符的时候首先根据选择子的Index找到段描述符,在解析描述符P是否=1 如果为0直接失败 返回0xC0000005
2.如果加载数据段段描述符 段会检查 RPL<=DPL 和CPL<=DPL
3.在访问内存的时候段会检查,是否可写,如果W=0不可写,发送了写入操作段会制止你给一个异常0xC0000005访问异常
4.在访问内存的时候段会检查,是否在段范围内,如果也会制止你给一个异常
加载一个段描述符在CPU找到这个段描述符后,会进行检查P=1的检查如否则直接失败返回一个0xC0000005,然后检查
你有没有权限加载这个段 如果是数据段要求 RPL<=DPL&&CPL<=DPL(上述有证明) ,如果这两个检查过了就会把段描述符的中的Base,Limit Atrribute; 这80位加载到CPU内部的段寄存器缓冲中(你用哪个段寄存器操作的就加载到那个段寄存器),加载后对操作内存的指令在执行的时候都会检查这个段有没有写权限等等,有没有越界超出段的范围进行检查。
同上上面各种实验相信各位朋友对段的保护机制都有一定的了解了,ds es ss fs都是数据段寄存器,fs我们暂时不提,这个段急不同时在内核,还是用户层,都是用于访问一些特殊的结构体的
ds,es,ss在程序启动的时候段描述符就加载了 Base都是0x00000000 Limit都是0xFFFFFFFF 段的大小都是4GB
所以他们对内存的保护要么都让这4GB可读,要么全都可读可写,这样看的话肯定感觉有点扯,这么大内存以一种数据段的方式访问,之能有一种权限太笨重,所以需要一种机制,能将让数据段,有一部分可读的,有一部分时可写的,把内存风味一小块一小块的,所以页机制它来了,对内存的保护主要是页机制(并不是说段机制没有保护内存),上面的实验大家也都看到,除了我们学底层的人可能才偶尔写程序的时候修改段寄存器(非常非常少 只有做实验验证机制的时候),其他时候都是使用段寄存器都是加载好的数据段的Base都是0 Limit都是0xFFFFFFFF,段大小都是4GB(这里暂时不考虑fs,fs有自己的作用),都是可读可写的,模式es=ss=ds =23 3环的嘛(不同系统可能有所不同),我们知道内存中有一些常量区,比如字符串常量就存储在常量区,只读的,一个段机制肯定满足不了的,页机制可以在段机制的基础上,把虚拟内存分页一块块,有的可读,有的可写,也有的根本没有挂上物理页(相当于根本不存在比如0地址就挂上物理页,后面我们会做实验,我们能够把0地址挂上物理页,也能够让0地址和其他任务地址不管是本进程还是其他进程的地址实现映射),所以对内存的保护大部分都是页机制,段机制做的比较少,改变段寄存器的值的时候,段也会有权限检查,希望将这么多能让各位朋友对段有一个清楚的认识,上面的有一个实验,同一个虚拟地址用ds段访问那个地址就挂了,es访问没有问题,这就是段的保护,使用什么段区访问将遵循这个段的属性(Limit Base 属性),我还可以与在上面的例题构造一个数据段描述符(Base=0,Limit=0xFFFFFFFF,只读)加载到fs中,这样使用fs访问内存都是只读的,只要写入必挂,(给大家一个提示如果给ss段寄存器加载的数据段描述符是只读的必挂,因为ss比较特殊 它的Push 指针如果没有写权限直接就挂了,加载可读可写的数据段就没有问题),希望大家理解的是这个设计思想,而不是单纯的记住这个特例,后面还会给大家详细的带来页的保护机制,其实很多人学完段过了一段时间就根本不知道段做了什么,页机制就不会感觉更加的清晰可见,下一篇见