为了更好管理更大的内存,现代操作系统和CPU硬件引入了保护模式。其中保护模式的分页机制通过内存管理单元(MMU,Memory Management Unit)实现了物理地址到线性(虚拟)地址的转换,这个转换过程也称地址翻译。而本文探讨线性地址到物理地址的转换过程,并通过实际操作来体现。
实验环境:
- win 10
- windbg Preview
- VirtBox+win xp(切换PAE状态)
实验环境主要涉及Windbg通过VirtBox实现内核双机调试和本地内核调试。
具体理论部分可参考Intel手册中关于Paging的章节:https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-manual-325462.html
涉及到的概念
控制寄存器:
- cr0:分页机制的开关,PE位代表保护模式。PG位代表分页机制
- cr3:分页数据结构的基地址,cr3中存储的物理地址与分页模式有关
- cr4:控制硬件虚拟化设置(Pentium及80486以后才有)
PAE:Physical Address Extension,物理地址扩展。
查看系统是否开启了PAE(以xp为例),我的电脑-系统属性-常规:计算机;或者查看CR4寄存器的5位是否为1。
对于在长模式(long mode)中的x64来说,PAE是必须的。PAE改变了传统的保护模式分页机制,增加了一级页,CR3不再指向页目录物理段地址,而是指向页目录指针表,即一个包含4个页目录指针的表。
分页模式
启用分页模式条件:cr0.PG = 1 且 cr0.PE = 1
根据不同CPU架构及特性主要分为三种模式,处于哪种模式视寄存器属性不同:
- 32-bit paging(32位OS): cr0.PG = 1 ; cr4.PAE = 0
- PAE paging(32位OS且开启了PAE): cr0.PG = 1 ; cr4.PAE = 1 ; IA32_EFER.LME = 0
- IA-32e paging(64位OS): cr0.PG = 1 ; cr4.PAE = 1 ; IA32_EFER.LME = 1
需要注意的是:
- 32bit下,每个entry(表项)是4字节大小;而在PAE和IA-32e下,每个entry是8字节大小
- 在x64体系中只实现了48位的virtual address,高16位被用作符号扩展,这高16位要么全是0,要么全是1。所以在讨论64bit地址的时候,高16位不使用
Paging相关数据结构
- PML4T:The page map level 4 table,每个表为4kb,内含512个PML4E结构。这个表的物理基址存储在cr3[12:51]中(IA-32e特有)
- PML4E:The page map level 4 entry,PML4表项,每个8字节大小(IA-32e特有)
- PDPT:Page Directory Pointer Table,页目录指针表,每个表4kb,内含512分PDPTE结构(PAE和IA-32e特有,PAE下这个表的物理地址存储在cr3[5:31]中)
- PDPTE/PDPE:Page Directory Pointer Table Entry,页目录指针表项,每个8字节(PAE和IA-32e特有)
- PDT:Page Directory Table,每个表4K,内含512个PDE结构
- PDTE/PDE:Page Directory Table Entry,页目录表的表项
- PT:Page Table,每个4kb,内含512个PTE结构
- PTE:Page Table Entry
这里需要注意的是各种table及entry的描述性数据结构,在实际操作中,其低12bit都置0处理,因为这部分是用来描述页表属性的。
地址转换基本原理
原理很简单,就是讲线性地址拆分,各个域对应不同表,表项的index,然后来一步步定位。随着32bit OS到64bit OS的转变,32bit地址到64位地址的转变,地址转换的层级也就逐步增多,从二级页表(32bit)到三级(PAE),再到4级(64bit)。
页转换模型
IA-32e提供了三种页转换模型:
- 4k:PML4t,PDPT,PDT和PT
- 2M:PML4T,PDPT和PDT
- 1G:PML4T和PDPT
本文所有分页模式下的转换都是基于4K的寻址方式,因为在个人计算机上,普遍是4K,4K对齐嘛
32bit地址
32bit的线性地址到物理地址转换最简单,是通过二级页表来进行的。此时cr3高20位中存储的是页目录表物理基地址
具体步骤:
- 将32bit地址拆分为三部分:高10位(PDE index),中间10位(PTE index),低12位(物理页offset)
- 获取页表的物理基地址:PDE index 4 + cr3中页目录表基地址= PT base addr,通过这个指针即可找到页表的物理基地址
- 获取分配到的物理页地址:PTE index * 4 + PT base addr,就可以找到存储物理页地址的页表项
- 物理页地址+物理页offset=物理地址
这里为什么乘以4呢,因为每个entry 4字节大小
以未开启PAE的win xp下calc.exe为例,其入口地址为0x1012475,这个地址怎么来的呢,OD载入即可找到。因为没有ASLR,所以每次运行的地址是固定的。将0x1012475分成三部分:
- 0x10
- 0x12
- 0x475
根据上面两张图,可以看出物理地址0x145e8375处数据和0x1012475处数据一致
为了进一步验证是否数据是否一致,在Windbg下修改物理地址处数据,然后通过OD查看虚拟地址处的数据,发现也是一致变化的。
开启PAE
此时线性地址要拆分成四部分,高2位(PD index),中高9位(PT index),中的(PTE index),低12位(物理页偏移)。此时cr3[5:31]存储的是PDPT base addr。
开启硬件的PAE选项,可以发现XP下多了”物理地址扩展”
将0x1012475拆分成四部分:
- 0x0
- 0x8
- 0x12
- 0x475
具体转换步骤如下图:
这里为什么乘以8呢,因为PAE下每个entry 8字节大小
IA-32e地址转换
此时64bit线性地址要拆分成五部分:
- [51:42] (PML4E index)
- [41:32](PDPE index)
- [31:22](PDE index)
- [21:13](PTE index)
- [12:0](physical offset)
此时cr3[5:31]存储的是PML4T base addr。
用以下这张图可以清晰明了表示:
这里测试写了一段代码1
2
3
4
5
6
7
8
9
10
11
12
13#include <cstdio>
#include <iostream>
using namespace std;
int main()
{
char szName[20] = "HelloWorld";
printf("szName:0x%x\n", szName);
cout << "szName:" << &szName << endl;
getchar();
return 0;
}
为什么这里要写个printf输出呢。因为在看雪论坛上看到一个帖子啊,讲x64位虚拟地址转换,用printf直接输出值,这样是不严谨的,因为printf直接输出的话,只能输出32bit地址,如果是64bit的话,高32bit会被截取掉的。看雪上那篇帖子测试的时候正好输出了32bit的地址,所以可以寻到物理地址。
这里将szName的地址:0x2f440ff628分成五部分:
- 0x0
- 0xbd
- 0x20
- 0xff
- 0x628
具体转换步骤如下图:
同样这里乘以8也是因为每个entry 8字节大小
Windbg有一个扩展指令,!vtop可将虚拟地址转换成物理地址,在上图中也有验证。
参考链接
启用PAE后虚拟地址到物理地址的转换:https://bbs.pediy.com/thread-180989.htm
X64下的虚拟地址到物理地址的转换:https://bbs.pediy.com/thread-203391.htm
理解 paging:http://www.mouseos.com/arch/paging.html