今天开始正式学习分页的相关知识

分页与物理地址

什么是物理地址

在学习二进制的时候就有区分过物理地址和虚拟地址这两个概念,其实就是内存条真正的地址,这里不再赘述。而学习保护模式我们知道,实际的线性地址 = 逻辑地址+段寄存器.base,在汇编和C指针层面所使用的地址都是逻辑地址。但是似乎它等同于虚拟地址(线性地址),这是因为通常情况下段寄存器的 base 都为 0。

10-10-12分页详解

基本结构

拿到一个32位的地址,将其结构进行拆分

这就是 10-10-12 分页的命名由来,而去寻找对应的物理页也是根据每个进程的 Cr3 寄存器的值去找到,整体分页结构如下图所示

同时我们也可以看到,不同的虚拟页是可以映射同一个物理页的,下面来介绍一下 PDE 和 PTE 的基本结构。

物理页的属性

下面描述一下各个位的作用:

  • P 位:表示PDE或者PTE是否有效,如果有效为1,反之为0
  • R/W 位:如果R/W = 0,表示是只读的,反之为可读可写。
  • U/S 位:如果U/S = 0,则为特权用户(super user),即非3环权限。反之,则为普通用户,即为3环权限。
  • PS位:这个位只对PDE有意义。如果PS == 1,则PDE直接指向物理页,不再指向PTE,低22位是页内偏移。它的大小为4MB,俗称“大页”。
  • A 位:是否被访问,即是否被读或者写过,如果被访问过则置1
  • D 位:脏位,指示是否被写过。若没有被写过为0,被写过为1
  • G 位:表示是否为全局页。它的作用是什么呢?举个例子,操作系统的进程的高2G映射基本不变,如果Cr3改了,TLB刷新重建高2G以上很浪费。所以PDEPTE中有个G位,如果为1,刷新TLB时将不会刷新它指向的页。
  • PWT 位:当PWT = 1,写缓存的时候也要将数据写入内存中。
  • PCD 位:当PCD = 1时,禁止某个页写入缓存,直接写内存。比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了。

需要注意的是

  • PTE 可以没有物理页,且只能对应一个物理页。
  • 多个PTE也可以指向同一个物理页。
  • PDEPTE重合的属性共同决定着最终物理页的属性。比如 P 位,如果有一个是 0,那么最终的物理页就是无效的。但是PDEPTE它们的属性的影响范围是不一样的。数值上:物理页的属性 = PDE属性 & PTE属性。

地址翻译

对于一个给定的线性地址,比如我们执行mov eax,ds:[0x12345678]这句汇编指令的时候,0x12345678这个线性地址会传递给CPU,先查询TLB(页表缓存)和缓存有没有,有的话直接取出来返回;如果没有,经过 MMU(Memory Management Unit,内存管理单元)处理得到物理地址,通过固定的分页模式直接找到,取出数据返回。

TLB大概就是一张表,根据线性地址和 CR3 的值去找物理页地址,如果命中,MMU将可以快速完成翻译。如果 TLB 没有对应的项,则往后会查页表的缓存,查到了也会快速返回,否则只能够去一次一次访存查询页表(这里具体的过程还没有特别弄懂,mark一下,写了新的回来补)。

上面我们经历了一遍翻译的过程,具体我们需要给入几个参数:页表基址(物理地址),线性地址,数据,寄存器,读写信号,这也就解释了为什么 CPU 不允许直接交换两个位置的内存,如果允许则这方面处理将比较麻烦,何况并不是说不提供交换内存的指令就无法实现一些功能。

页表学习准备

笔者用的系统是 Win7 32 位的,虽然现在 32 位的系统几乎用的很少了,但是为了系统的学习还是从简单的开始。Win7 32 默认是 2-9-9-12 分页,先用几个指令配置为更简单的 10-10-12 分页。

1
2
bcdedit /set pae ForceDisable  
bcdedit /set nx AlwaysOff

如果要关闭那么用下面两个指令

1
2
bcdedit /set pae forceEnable
bcdedit /set nx OptIn

这样就回到了 2-9-9-12 分页。

  • 如果是2-9-9-12分页运行的内核 ntkrnlpa.exe
  • 如果是10-10-12分页运行的内核 ntoskrnl.exe

地址翻译练习

但是物理地址与之完全不一样,先可以尝试通过 windbg 练习寻找物理地址,这里使用记事本配合CE。

那么得到线性地址是 0029DFB0,根据 10-10-12 分页规则,将页表拆开

1
0000000000 1010011101 111110110000

这三部分分别是,页目录项(PDE),页表项(PTE),页内偏移(offset)。

找到页表基址,获得页目录项的地址,即 DirBase+4*PDE。在这里页表项地址就是 DirBase,随后跟过去,这里需要注意,这个地址并不是真的地址,而是因为每个页表项它有一定的大小。可能装下一个页目录项就要完整的一个页,如果是这样就决定了页目录项的地址一定是页对齐的(最低三位十六进制地址为0),所以我们找到的地址再把最低三位十六进制置为0就是真实的页目录项了,而通过前面的学习也可以看到,最低的十二个位被赋予了特殊的含义。

我们找到的该内存页目录项地址实际上就是 ad688000,然后再页目录项中找到页表项 1010011101 转为十六进制得到 29d,同样的,页目录项地址+4*PTE 得到页的物理地址,也就是 ad688a74

同样的,这里低三位十六进制也不是真实的地址,需要清零得到该页的页表基址,最后再加上页内偏移,也就是 FB0。

成功找到对应的物理内存。

而我们的 CR3 寄存器存储的就是页目录表的物理地址,即 Page Director Table,那么所说的 PDE 当然就是 Page Director Entry了。PTE 就是 Page Table Entry,对应的每个目录项指向的是一个 PTT,Page Table Table。

页目录表基址与页表基址

由于物理地址对操作系统是不可见的,所以操作系统必须有线性访问页表的能力,唯一的办法就是将自身的页表挂在一个特殊的地址上,这个地址是 0xC0300000。

也可以发现,CR3 对应的物理地址与线性地址 0xC0300000 是一致的。

相同与页目录表基址,为了在程序内快速访问页表,也有一个页表基址,页表基址对应的线性地址是 0xC0000000。

这里记一下通过线性地址寻找PTE和PDE的公式。

  • 访问页目录表的公式:0xC0300000 + PDI * 4
  • 访问页表的公式:0xC0000000 + PDI * 4096 + PTI * 4

很有趣的是,你会发现如果你要寻找 0xC0300000 对应的页表,用第二个公式代入就是它本身。即

1
2
3
4
5
0xC0300000

高10位: 1100 0000 00 = 0x300
中间10位: 1100 0000 00 = 0x300
低12位: 0000 0000 0000 = 0x000

代入第二个公式可得 0xC0000000 + 0x300 * 4096 + 0x300 * 4 = 0xC0300000

参考文献