保护模式下内存管理方式分为两种,段与页。前面的篇章中,简要介绍了段的知识,今天就来和大家聊聊页的知识,页是保护模式中更为重要的一环,随着系统进入32-Bit,段的作用明显降低了,取而代之的则是在段的基础上,更为细分的页。
段与页
这是Intel白皮书中介绍关于段与页的概要图,经过段的学习,可以很容易的理解左半部分,这是一个根据所提供的有效地址(图中Offset)以及段寄存器中确定的基址,锁定线性地址空间中的某个线性地址(图中Lin.Addr.)的过程。而右半部分,则是利用了页的功能,通过拆分线性地址,一步步转化成了物理地址。
上述提到了3个概念,有效地址,线性地址以及物理地址,文字叙述会让人混淆,我们来看一条语句:
1 | mov dword ptr ds:[0x12345678], 0x123 |
其中,0x12345678是有效地址
ds.Base + 0x12345678是线性地址
这都非常好理解,那什么是物理地址呢?考虑这样一个问题,掌握3环知识的小伙伴们知道每个进程都有4GB的内存空间,这时如果有一个进程A,给会进行一个操作,给ds.Base + 0x12345678赋值0x123,还有一个进程B,同样会给ds.Base + 0x12345678赋值0x123,那么ds.Base + 0x12345678处的位置到底是哪个值呢?还是两者都不是呢?这就涉及到了物理地址的概念。
PDT与PTT
每个进程都有一个CR3的值,这很突兀,CR3是什么?实际之前在TSS切换时也用到了,具体等到了控制寄存器那会详细分析。简单来说,CR3被用来切换和定位当前正在使用的页表,它是一个32位的寄存器,其中高20位指向一个物理页(Windows系统上,一个页的大小是4KB,也就是4096个字节),如下图所示:
这图该怎么看呢?首先CR3会指向一个物理页,这个物理页又叫做页目录表(PDT),页目录表每个元素叫做页目录表项(PDE),页目录表项,每个4字节,所以一共有1024个页目录表项,CR3就好比一本书,PDT就是这本书的目录,PDE就是章节,这是一本有1024个章节的书(哇塞,真厚啊U•ェ•*U),这样就好理解多了。这是第一级。
页目录表项又指向一个第二级的表,叫做页表(PTT),页表的大小也是4KB,页表中的每个元素叫做页表项(PTE)。页表项可以理解为书中章节的每个小节,就好比第一章里面有1024个小节,这个小节就是PTE,这1024个小节加起来,构成一个小节表,就是PTT。
第二级介绍完了,第三级也就好理解了,既然书中每个章节的每个小节理解了,接下来就是页码了,每个小节都会对应书中的某个页码。而这个页码,就是相当于的物理页了。这样就可以理解这张图了,就是一部部找到物理页。
10-10-12分页
Windows采用三种分页方式,在32位系统上主要有10-10-12分页和2-9-9-12分页这两种方式,在64位系统上增加了9-9-9-9-12这种分页方式,后面的文章会依次介绍32位下的两种分页方式。首先从10-10-12开始。
首先修改C盘的boot.ini文件,将noexecute改成execute,重启虚拟机,即可使用10-10-12分页方式
10-10-12分页是如何工作的呢?来看一个简单的程序:
1 |
|
这个程序很简单,给a赋值0x123,并查看a的地址
而这个地址0x12ff7c,实际上就是前文提到的线性地址,接下来我们将这个32位的地址按照10-10-12的方式进行拆分:
1.将0x12ff7c拆分成二进制:0000 0000 0001 0010 1111 1111 0111 1100
2.将这32位二进制数,按照10-10-12的方式组合:
每部分位数 | 二进制 | 十六进制 |
---|---|---|
10 | 0000 0000 00 | 0 |
10 | 01 0010 1111 | 12f |
12 | 1111 0111 1100 | f7c |
3.根据Cr3找到页目录表(PDT)中的页目录表项(PDE):
首先在Windbg中执行!process 0 0指令,找到当前程序的Cr3,Cr3的值指向的就是页目录表的首地址。由于第一部分值为0,所以要查找的PDE,需要用Cr3+0*4(乘4是因为每个PDE大小是4字节),这里注意一下,由于查找的是物理地址,所以使用的是!dd指令。
4.根据PDE找到页表(PTT)中的页表项(PTE):
上一步已经找到了PDE,PDE的值指向的是某个PTT的首地址,方法和上一步一样,用PDE中的值+12f(拆分完的第二部分)x4,就可以得到PTE,这里要注意一点,将PDE中的值代入时,后12位置0,由于后12位为属性位,在查找的过程中不起作用
5.根据PTE确定物理地址:
确定PTE后,就剩最后一步了。由于一个物理页的大小本身就是4KB,也就是2的12次方,所以当确定了前20位后也就确定了物理页,因此我们要找的内容就在219da000这个物理页上的某个物理地址。这个物理页的范围是219da000~219dafff。现在可以理解,PTE指向的是一个物理页的首地址,根据最后12位的来确定,我们要寻找的值在物理页上的偏移,也就真正的找到了这个物理地址。
根据实验截图,发现我们一开始存在变量a里面的0x123,真正存的地方在0x219daf7c这个物理地址的位置,这就是通过线性地址一步步的找过来的,这些工作都是CPU做的,比如当我们读取a这个地址上的值是,CPU会通过分页机制读取该物理地址的值,然后再显示出来。
有趣的实验
读错值了?
有了10-10-12分页的知识,来做一个有趣的小实验
1 |
|
这个代码非常简单,一般人认为,会先输出a的地址,然后再输出0x123。但有了物理页的知识,我们就可以做一些手脚了。
很奇怪吧?为什么输出不是0x123,而却输出0x456呢?原因就在于,我们偷偷修改了变量的物理地址上的值,将原本的0x123改成了0x456,因此,CPU再次去物理页读取时,值已经发生了变化,读到了修改后的值。
0地址也能存值?
1 |
|
这是个显而易见运行会失败的程序,为什么?因为你给0地址赋值了,有点C/C++开发经验的人都知道,0地址是不能存值的,为什么?因为运行不过去啊!这不扯淡嘛!你看我就运行过去了!
这又是为什么呢?其实0地址不能存值的原因是,没有给它挂物理页,既然没有物理页,那CPU按照分页去查的时候,就查不到了;那么只要给他挂个物理页,就可以给这个线性地址存值了,具体操作如下。
总结
- 一张页表能包含的物理页:1024*KB = 4MB
- 10-10-12分页共有1024张页表:1024*4MB = 4GB
- 前20位的值如果相同,那么一定在同一个物理页
- 一个PTE最多可以指向一个物理页;PTE可以没有物理页;多个PTE可以指向同一个物理页
参考资料:《Intel白皮书第三卷第四章》