1.概述
一个进程的虚拟地址空间是4GB,它的物理大小并不是真的4GB,如果真的是4GB,那运行中的所有进程都加起来占用的空间是一个天文数字,事实上系统会对这4GB的地址空间进行转换,然后映射到物理内存中。换句话说虚拟地址本身并不是真实存在的。
将虚拟地址转换为物理地址依赖的规则就是分页机制。
物理地址也并不是内存条地址,物理地址需要再进行一层转换才能找到真正的内存条地址。
我们只要会转换到物理地址就够用了。
1.1 逻辑地址
假如有指令:
MOV eax,dword ptr ds:[0x123456]
0x123456就是逻辑地址
1.2 线性地址
线性地址是对于虚拟空间来说的,还是上面的例子,ds.Base + 0x123456就是线性地址
1.3 物理地址
通过线性地址可以转换为物理地址。
1.4 分页模式
分页模式分为10-10-12和2-9-9-12,前者最多可以支持4GB内存,后者在32位操作系统下可以支持16GB内存,64位操作系统下可以支持128G内存,后者也叫PAE分页。
在Windows XP下,找到C盘下的boot.ini文件,将里面的noexecute改为execute即可将PAE分页改为10-10-12分页。
2. 10-10-12模式
2.1 10-10-12模式概述
一个32位的虚拟地址会被解释为三个部分:
- 页目录索引(PDT,10位)
- 页表索引(PTT,10位)
- 字节索引(12位)
系统中有一个CR3寄存器,它存储了页目录索引的地址,页目录占4KB,里面的成员占4个字节,因此有1024个成员,PDT里面的成员被称作PDE,PDE里面保存的是一个32位的物理页地址。
每个PDE又指向一个页表索引,页表索引里面的成员被称作PTE,PTE可以没有物理页(线性地址0一般就没有),多个PTE也可以指向同一个物理页。
10-10-12结构的第1个10是PDT中的索引,第2个10是PTT中的索引,最后的12是指物理页上的偏移,所以简单来说转换成物理地址只需要将10-10-12的结构分别在PDT和PTT中找到对应的位置再加上物理页的偏移就是最终的物理地址了。
(上面的图其实不是很准确,不过为了便于理解先以这个图为准吧,其实页目录表也是页表,只不过它比较特殊,后面会介绍)
2.2 线性地址转物理地址实验
本实验是在10-10-12模式下进行的,因此要将PAE关闭,PAE就是2-9-9-12模式,可以简单地理解它为增强型的分页模式。
bcdedit /set pae forcedisable
重新打开PAE模式
bcdedit /set pae forceenable
1.找到数据在内存中的线性地址
- 新建一个.txt文件,文件中 有字符串“hello,world”,并保持.txt文件处于打开状态。
- 使用CE附加到notepad.exe
- 在Text栏中搜“hello,world”,找到可以找到多个跟它有关的地址,通过修改.txt文件中的字符串最终筛选出真正的内存地址0x000AA8A0,注意勾选上Unicode以及Value Type的类型,如下图所示:
在进程中查找字符串还有一方法:
- 在WinDbg中执行!process 0 0 notepad.exe
- 继续执行 .process 9050c4e0,切换到记事本进程中
- 执行s -u 0x00000000 L0x01000000 "hello,world"搜索内存中的字符串
- 结果可能会有多个,但是只有一个是正确的线性地址,可以使用du 0x28e090指令查看Unicode字符串。
2.分解线性地址
000AA8A0转成二进制是32位,刚好可以按照10-10-12的格式分解成:
0000 0000 00
0010 1010 10
1000 1010 0000
3.根据分解后的线性地址找到物理地址
-
首先在WinDbg中使用!process 0 0,在一堆进程中找到记事本进程,然后可以找到它的CR3寄存器0x146a0000,这个地址就是PDT的基址,它保存的是物理地址,所以下面的指令都是以!开头的。
-
10-10-12格式的第1个10部分是0000 0000 00,这个值指向了它在PDT中的下标,所以使用!dd 146a0000+0找到对应的PDE。
-
10-10-12格式的第2个10部分是0010 1010 10,上面找到的PDE是0x14dc0067,因此通过使用!dd 14dc0000+aa*4找到对应的PTE,为什么不是14dc0067而是14dc0000?因为PDE的最后12位是属性,下面PTE的末尾12位同样也是属性,PDE的属性&PTE属性=物理页的属性。
PDE和PTE的属性如下:
P:1可用,0不可用
R/W:R/W=0只读,R/W=1可读可写
U/S:0特权用户,1普通用户
A:是否被访问,访问置1
D:是否被写过,写过置1
P/S:PageSize,只对PDE有意义,1直接指向物理页无PTE,低22位是页内偏移,页大小为4MB,俗称为“大页”
PAT:只对PTE有意义
G:CR3改变时,TLB会立即刷新,但不会刷新PDE/PTE的G位为1的页,一般高2G线性地址G为1,因为高2G是系统通用的,刷新影响效率
PWT、PCD、PAT在下篇介绍PAE时的时候会解释。
0010 1010 10对应aa,乘以4的原因是每个元素占4个字节
- 找到的PTE是0x13fdd067,最后再加上物理页偏移1000 1010 0000(转换成十六进制是0x8a0)就是最终的物理地址了,使用!dd 13fdd000+8a0。
- 使用上面的指令还看不出是字符串“hello,world”,使用!db 13fdd000+8a0就可以看到字符串了
2.3 页目录表基址
系统要保证一个线性地址有效,必须提前将这个线性地址对应的PDE和PTE填充,那么系统是怎么填充PDE和PTE的呢?
PDE和PTE都是物理页,我们在程序中是无法直接访问物理页的,我们要访问任何东西必须要通过线性地址。
系统通过0xC0300000这个线性地址找到的物理页就是页目录表,这个物理页比较特殊,它是页目录表PDT,但是本身也是页表,即PTT,换句话说,其实所谓的PDT表本身也是一个PTT表。0xC030000指向了PDT,所以系统可以通过它填充PDE,PTE是怎么填充的呢?看下一节页表基址
2.4 页表基址
系统通过0xC0000000访问第一个PTT,0xC0001000指向第二个PTT。
一个4GB的进程中只能有一个PDT(上面说过,其实PDT是一种特殊的PTT),但是有1024个PTT,每个PTT都占4KB。
总结一下:
- 整个页表占4M地址空间,从0xC0000000到0xC03FFFFF,页表有1024个成员,每个成员都是一个PTT,占4KB。
- 在这1024个成员中,有一个PTT比较特殊,它就是PDT。
真正的页表是这样的:
访问页目录表的公式:
0xC0300000 + PDI * 4(PDI是页目录表的索引)
访问页表的公式:
0xC0000000 + PDI * 4096 + PTI * 4(PDI * 4096是因为每个PTT的大小是4K,PTI是页表的索引,PTI*4,每个PTE占4个字节)