ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深入解析x64架构9-9-9-9-12分页机制:从虚拟内存到物理地址转换

深入解析x64架构9-9-9-9-12分页机制:从虚拟内存到物理地址转换 1. 从线性地址到物理地址为什么需要分页如果你写过用户态的程序可能对虚拟内存的概念并不陌生。操作系统为每个进程提供了一个独立的、连续的虚拟地址空间比如在64位Windows上用户态进程通常能看到一个从0到0x7FFFFFFFFFF128TB的地址范围。这个地址是“虚拟”的它只是一个编号并不直接对应物理内存条上的某个芯片单元。当你访问这个地址时CPU和操作系统会联手通过一套复杂的查找机制将这个虚拟地址“翻译”成真实的物理地址。这套翻译机制的核心就是分页。那么为什么需要这么麻烦直接把物理地址给程序用不行吗这背后有几个关键原因内存隔离与保护如果没有虚拟内存所有程序都运行在真实的物理地址上。一个程序的Bug比如数组越界就可能覆盖掉另一个程序甚至操作系统内核的数据导致系统崩溃。分页机制让每个进程都活在自己的“沙箱”里互不干扰。简化内存管理程序员和编译器无需关心物理内存的实际布局和碎片问题。他们可以假设自己拥有一个从零开始、连续且巨大的内存空间这极大地简化了软件开发。实现内存超售Overcommit物理内存是有限的但所有进程的虚拟内存总和可以远超物理内存大小。操作系统通过将暂时不用的内存页面交换到硬盘上如页面文件可以“假装”拥有比实际更多的内存。共享内存不同的虚拟地址可以映射到同一块物理内存。这使得系统DLL动态链接库只需在物理内存中加载一份就能被所有进程共享节省了大量内存。在x64架构下这套翻译机制的具体实现就是我们今天要深入探讨的“9-9-9-9-12分页”。这个名字听起来像一串神秘代码其实它精确地描述了从64位虚拟地址到物理地址的“寻路”规则。理解它是理解Windows x64内核内存管理、驱动开发、漏洞利用乃至安全防护的基石。接下来我们就一层层剥开它的外壳。2. 9-9-9-9-12分页的寻址结构拆解“9-9-9-9-12”这五个数字直接对应了x64分页结构中五级页表的索引位宽。要理解它我们得先看看一个完整的64位虚拟地址在分页视角下是如何被“切割”的。理论上x64架构有64根地址线可以寻址2^64 16EB艾字节的巨大空间。但目前的CPU和操作系统并没有使用全部64位。在Windows x64中目前只使用了48位进行虚拟地址寻址这提供了256TB2^48字节的虚拟地址空间。这48位被划分为用户空间和内核空间各128TB。一个有效的x64虚拟地址规范地址高16位bit 63到bit 48必须是全0或全1这被称为符号扩展。实际用于寻址的是中间的48位bit 47到bit 0。这48位就是被“9-9-9-9-12”分页机制瓜分的对象。让我们把这48位地址拆解开来看| 保留 (16位) | PML4索引 (9位) | PDPT索引 (9位) | PD索引 (9位) | PT索引 (9位) | 页内偏移 (12位) | |------------|----------------|----------------|--------------|--------------|-----------------| | 63:48 | 47:39 | 38:30 | 29:21 | 20:12 | 11:0 |现在我们来逐一解释每一部分的含义和作用### 2.1 页内偏移 (12位)这是地址的最后12位bit 11-0。它表示在找到最终的物理页面Page Frame后具体要访问该页面内的哪个字节。2^12 4096这意味着一个物理页面的大小是4KB。所以页内偏移的范围是0到4095。无论中间经过多少级页表翻译最终定位到的物理内存块都是以4KB为单位的页内偏移就是在这个块内进行精确定位。### 2.2 四级页表索引 (各9位)剩下的36位bit 47-12被平均分给四个页表级每级9位。PML4索引 (9位)用于在第一级页表PML4表中查找项。PML4全称是Page Map Level 4。PDPT索引 (9位)用于在第二级页表PDPT Page Directory Pointer Table中查找项。PD索引 (9位)用于在第三级页表PD Page Directory中查找项。PT索引 (9位)用于在第四级页表PT Page Table中查找项。每一级索引都是9位意味着每一级页表都有2^9 512个表项。每个表项在x64下是8字节64位。因此一张完整的页表大小就是 512项 * 8字节/项 4KB。巧了正好和一个物理页面的大小相同这不是巧合而是精心设计的结果使得操作系统可以用管理内存页的同一套机制来管理页表本身非常优雅。### 2.3 翻译过程一场逐级深入的寻宝游戏想象一下CPU要翻译虚拟地址0xFFFFF80012345678。第一站CR3寄存器。这是一个特殊的CPU控制寄存器里面存放着当前进程的PML4表的物理地址。这是整个翻译过程的起点。CPU从CR3指向的物理内存位置即PML4表开始。第二站PML4表。CPU取出虚拟地址的bit 47:39PML4索引假设是i。它就在PML4表的第i个表项每个表项8字节。这个表项里存放着下一级页表PDPT的物理地址实际上是物理页框号我们稍后解释。第三站PDPT表。CPU拿着上一步得到的PDPT物理地址找到PDPT表。再取出虚拟地址的bit 38:30PDPT索引j找到该表的第j个表项。这个表项里存放着再下一级页表PD的物理地址。第四站PD表。同理用bit 29:21PD索引k在PD表中找到第k个表项里面是页表PT的物理地址。第五站PT表。最后用bit 20:12PT索引l在PT表中找到第l个表项。这个表项至关重要它里面存放的才是最终目标物理页面Page Frame的物理地址同样是物理页框号。终点物理内存。将PTE中给出的物理页框地址加上最初虚拟地址中的12位页内偏移就得到了完整的物理地址。CPU终于可以用这个地址去访问物理内存了。这个过程可以抽象为CR3(物理地址) - PML4E - PDPTE - PDE - PTE - 物理页框地址 偏移 最终物理地址。注意这里为了简化我们假设了所有级别的页表都“存在”。实际上页表项中有标志位如P位 Present来指示下一级页表或最终页面是否在物理内存中。如果不存在会触发缺页异常Page Fault由操作系统介入处理例如从磁盘调入页面或分配新的物理页。3. 页表项PTE的奥秘不仅仅是地址上面我们反复提到每一级页表项里存放着“下一级页表或最终页面的物理地址”。但在x64架构下一个物理地址有52位理论上可寻址4PB空间。而一个页表项是64位8字节难道只用了其中52位来存地址吗当然不是。剩下的位被用来存储丰富的控制和管理信息。理解这些标志位是理解内存保护、属性设置和高级内存特性的关键。一个标准的x64页表项以最后一级的PTE为例的位域通常如下所示具体布局因CPU厂商和模式略有差异但大同小异| 物理页框号 (PFN) (40-52位) | 保留位 | 标志位 | 全局页(G) | 页属性(Pat) | 脏页(D) | 访问(A) | 缓存禁用(PCD) | 写通(PWT) | 用户/超级用户(U/S) | 可读/写(R/W) | 存在(P) | |---------------------------|--------|--------|-----------|-------------|----------|----------|----------------|------------|-------------------|---------------|----------| | 位 51:12 | 11:9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |我们来解读几个最关键、最常用的标志位P (Present 位0)存在位。为1表示该页表/页面在物理内存中为0则表示不在。访问P0的项会触发缺页异常。R/W (Read/Write 位1)读写位。为0表示只读为1表示可读可写。这是实现内存写保护的基础。U/S (User/Supervisor 位2)用户/超级用户位。为0表示该页面属于“超级用户”模式即内核态为1表示用户态可访问。用户态程序试图访问U/S0的页面会触发访问违例。PWT (Page Write-Through 位3)和PCD (Page Cache Disable 位4)控制该页面的缓存策略。PWT1表示写通缓存PCD1表示禁用缓存。这对映射内存映射I/OMMIO区域非常重要因为设备寄存器读写通常不能经过CPU缓存。A (Accessed 位5)访问位。当该页面被读或写时CPU硬件会自动将其置1。操作系统可以利用此位来实现页面置换算法如时钟算法找出最近未被使用的页面。D (Dirty 位6)脏位。当该页面被写入时CPU硬件会自动将其置1。这对于页面换出到磁盘至关重要只有被修改过的“脏”页面在换出时才需要写回磁盘干净的页面直接丢弃即可。PAT (Page Attribute Table 位7)与PCD、PWT一起用于选择更复杂的缓存类型通过IA32_PAT MSR模型特定寄存器配置。G (Global 位8)全局页位。为1表示该页面是全局的在任务切换更新CR3时TLB快表中关于此页面的缓存项不会被刷新。这常用于内核代码和数据可以提升系统性能。PFN (Page Frame Number 位 51:12)这是页表项的核心内容它存储了下一级页表或最终物理页面的物理页框号。物理地址 PFN 12 页内偏移。因为物理页面是4KB对齐的所以其物理地址的低12位总是0。页表项中只存储高位的PFN节省了空间。### 3.1 大页Large Page的支持x64架构除了标准的4KB页面还支持2MB和1GB的“大页”。使用大页可以减少页表级数降低TLB Miss快表未命中的开销对于需要大量连续内存访问的应用如大型数据库性能提升显著。2MB大页在PD第三级这一级实现。当PDEPage Directory Entry的PSPage Size 位7标志位为1时该PDE直接指向一个2MB的物理页面而不是下一级PT表。此时虚拟地址的划分变为PML4索引(9) | PDPT索引(9) | PD索引(9) | 页内偏移(21)。省去了最后一级PT的查找。1GB大页在PDPT第二级这一级实现。当PDPTE的PS位为1时它直接指向一个1GB的物理页面。虚拟地址划分变为PML4索引(9) | PDPT索引(9) | 页内偏移(30)。省去了PD和PT两级查找。在Windows中驱动程序或应用程序可以通过MmAllocateContiguousMemorySpecifyCache等API或内存管理标志来申请大页内存。4. 在Windows中观察与实践分页机制理论说得再多不如动手看看。在Windows内核调试环境下我们可以直观地窥探分页的细节。这里以使用WinDbg调试为例。### 4.1 查看CR3与进程页表基址每个进程都有自己独立的虚拟地址空间其秘密就在于每个进程的CR3值不同。CR3寄存器存放的是当前上下文PML4表的物理地址。在WinDbg中我们可以通过!process命令查看进程的目录表基址Directory Table Base它就是即将被加载到CR3的值。kd !process 0 0 notepad.exe PROCESS ffffaa0a3f4c7080 SessionId: 1 Cid: 0b50 Peb: 00f27000 ParentCid: 0f30 DirBase: 1a6eb000 ObjectTable: ffff9b0d3d0e5d00 HandleCount: 109. Image: notepad.exe这里的DirBase: 1a6eb000就是notepad.exe进程的PML4表物理地址注意这是物理地址。当CPU切换到该进程时这个值会被加载到CR3寄存器。### 4.2 手动解析虚拟地址假设我们想解析notepad.exe进程内一个虚拟地址0x7ff654321000的翻译过程。我们需要用到!vtop命令。但!vtop需要物理页表基址和虚拟地址。更直接的方式是使用.process切换进程上下文后用!pte命令。首先切换到目标进程的上下文kd .process /p ffffaa0a3f4c7080 Implicit process is now ffffaa0a3f4c7080 kd !pte 7ff654321000 VA 00007ff654321000 PXE at FFFFAA0A3F4C7000 PPE at FFFFAA0A3F4C7FE0 PDE at FFFFAA0A3F4FFD90 PTE at FFFFAA0A3FA79908 Unable to get pte at FFFFAA0A3FA79908!pte命令试图输出每一级页表项的虚拟地址。但有时对于用户空间地址在内核调试器中可能无法直接访问其PTE虚拟地址因为PTE所在页面可能不在当前上下文中。我们可以用另一种方法使用物理地址计算。我们知道进程的DirBase是0x1a6eb000。虚拟地址0x7ff654321000的各个索引计算如下注意这是用户空间地址高16位为0PML4 Index (0x7ff654321000 39) 0x1FF (0x7ff65) 0x1FF 0x65PDPT Index (0x7ff654321000 30) 0x1FF (0x3ffb2a) 0x1FF 0x1A8PD Index (0x7ff654321000 21) 0x1FF (0x7ff6543) 0x1FF 0x143PT Index (0x7ff654321000 12) 0x1FF (0x7ff654321) 0x1FF 0x121然后我们可以用!dq命令按物理地址显示来逐级查找。但手动计算非常繁琐。更实用的方法是利用WinDbg的!vtop命令它需要知道页目录基址的物理地址就是DirBase和要翻译的虚拟地址。不过!vtop在普通内核调试会话中可能受限。对于内核地址解析起来更直接。### 4.3 解析内核地址内核空间地址在所有进程中是共享映射的虽然每个进程有自己的PML4但内核部分的高半区映射是相同的。我们以内核模块ntoskrnl.exe中的一个地址为例kd lm m nt Browse full module list start end module name fffff8072c200000 fffff8072d029000 nt (pdb symbols) kd !pte fffff8072c200000 VA fffff8072c200000 PXE at FFFFAA0A3F4C7F78 PPE at FFFFAA0A3F4FFFD8 PDE at FFFFAA0A3F5FFB80 PTE at FFFFAA0A3E610100 PXE FFFFAA0A3F4C7F78 - 0A00000001067867 PPE FFFFAA0A3F4FFFD8 - 0A00000001068867 PDE FFFFAA0A3F5FFB80 - 0A00000001069867 PTE FFFFAA0A3E610100 - 8A0000002C22B863这次成功了!pte输出了每一级页表项的虚拟地址例如PXE atFFFFAA0A3F4C7F78和该项的内容例如0A00000001067867。这里的命名PXEPML4E (Page Directory Pointer Table Entry)PPEPDPTE (Page Directory Pointer Table Entry)PDEPDE (Page Directory Entry)PTEPTE (Page Table Entry)我们看最后一级的PTE内容8A0000002C22B863。这是一个64位的值。我们结合前面讲的PTE结构来分析注意Windows和Intel/AMD的位域定义可能略有不同需参考官方手册但基本原理一致低12位是0x863。二进制为1000 0110 0011。位0 (P): 1 - 页面存在。位1 (R/W): 1 - 页面可写。位2 (U/S): 0 - 超级用户内核页面。位5 (A): 1 - 已被访问。位6 (D): 1 - 已被写入脏页。位7 (PAT): 0位8 (G): 1 - 全局页。高位的物理页框号PFN是(0x8A0000002C22B863 12) 0x8A0000002C22B。这个值左移12位乘以4096再加上虚拟地址的低12位偏移0x000就得到了最终的物理地址。我们可以用!pfn命令查看这个物理页框的信息kd !pfn 8A0000002C22B PFN 0008A0000002C22B at address FFFFAA0A3F4C7000 ...### 4.4 一个常见的实战场景访问无效指针当你在驱动或内核代码中访问一个无效的指针例如NULL指针解引用时会发生什么假设我们访问0x0。kd !pte 0 VA 0000000000000000 PXE at FFFFAA0A3F4C7000 PPE at FFFFAA0A3F4C7000 PDE at FFFFAA0A3F4C7000 PTE at FFFFAA0A3F4C7000 PXE FFFFAA0A3F4C7000 - 0A00000001067867 PPE FFFFAA0A3F4C7000 - 0A00000001067867 PDE FFFFAA0A3F4C7000 - 0A00000001067867 PTE FFFFAA0A3F4C7000 - 0000000000000000注意最后一级的PTE内容是0000000000000000。其最低位P位为0表示该页面不存在。CPU在翻译到这里时发现P0会触发一个缺页异常Page Fault。操作系统这里是Windows内核的异常处理程序会检查这个地址。对于用户态的0x0地址它通常是一个受保护的、未映射的区域因此系统会抛出访问违例Access Violation在用户态表现为程序崩溃。如果是在内核态如驱动访问这样的地址则会导致系统蓝屏Bug Check通常是PAGE_FAULT_IN_NONPAGED_AREA。5. 分页机制对系统性能与开发的影响理解了分页的机制我们就能从更深层次思考它对系统性能和软件开发带来的影响。### 5.1 TLB加速翻译的缓存如果每次内存访问都要走一遍“CR3 - PML4 - PDPT - PD - PT”的四级查找那开销将是无法接受的。为了解决这个问题CPU内部有一个叫做TLBTranslation Lookaside Buffer的高速缓存。它缓存了最近使用过的虚拟地址到物理地址的翻译结果。当CPU需要翻译一个虚拟地址时它首先在TLB中查找。如果找到TLB命中就直接获得物理地址无需访问内存中的页表。只有TLB未命中时才需要执行完整的多级页表遍历这个过程称为“页表漫步”Page Table Walk并且新的翻译结果会被存入TLB。TLB的大小有限因此程序的局部性原理对性能至关重要。如果程序频繁跳转访问相距很远的内存地址导致TLB频繁失效性能就会下降。使用大页可以减少TLB项的数量因为一个2MB大页的TLB项可以覆盖512个4KB页面的范围对于具有大块连续内存访问模式的应用是重要的优化手段。### 5.2 写时复制Copy-on-Write这是现代操作系统实现进程快速创建如fork()和内存高效共享的关键技术。其核心思想是当父进程创建子进程时并不立即复制整个地址空间的物理页面而是让父子进程的页表项指向相同的物理页面并将这些页面标记为只读。当任一进程试图写入这些共享页面时CPU会检测到写入只读页面的操作通过PTE的R/W位触发页保护异常。操作系统异常处理程序会捕获这个异常然后为执行写入的进程分配一个新的物理页面复制原页面内容并更新该进程的PTE指向新页面且标记为可写。这样只有在实际需要写入时才会发生复制节省了大量内存和复制时间。### 5.3 内存映射文件Memory-Mapped Files分页机制使得将磁盘文件直接映射到进程的虚拟地址空间成为可能。当你使用CreateFileMapping和MapViewOfFileAPI时操作系统并没有将整个文件读入内存。它只是修改了进程的页表将一部分虚拟地址区域映射到特殊的“文件后备”页面。当你访问这些虚拟地址时如果页面不在内存中P0触发的缺页异常处理程序会从磁盘文件中将对应的数据块读入一个物理页面然后建立映射。同样当页面被换出时如果是脏页可以写回文件。这为处理大文件提供了极其高效的方式也是Windows执行体Executive管理可执行文件映像Image的基础。### 5.4 对驱动程序开发者的启示对于内核驱动开发者来说深刻理解分页是必须的区分分页与非分页内存内核地址空间分为分页池Paged Pool和非分页池Nonpaged Pool。可以被换出到磁盘的内存属于分页内存而必须在物理内存中常驻的如中断服务例程、某些关键数据结构必须分配在非分页内存。错误地将分页内存指针传递给必须在高IRQL下运行的代码如DPC、中断处理可能导致系统崩溃。物理地址与虚拟地址驱动与硬件交互时通常需要操作设备的物理地址如DMA缓冲区。你需要使用MmMapIoSpace将物理地址映射到内核虚拟地址空间才能访问。反之你也可能需要从虚拟地址获取其背后的物理地址MmGetPhysicalAddress虽然这需要谨慎使用。内存描述符列表MDLMDL是描述一块虚拟内存缓冲区的物理页面布局的结构。在进行DMA操作或需要锁定物理页面防止换出时需要用到MDLIoAllocateMdl,MmBuildMdlForNonPagedPool,MmProbeAndLockPages。访问用户空间内存驱动不能直接解引用用户模式传来的指针。必须使用ProbeForRead,ProbeForWrite检查可访问性并通过MmGetSystemAddressForMdlSafe等例程安全地访问。这是因为用户空间地址只在特定进程上下文中有意义且可能被换出或无效。6. 从分页看Windows内存管理内部Windows内存管理器是构建在硬件分页机制之上的一个复杂系统。了解分页有助于我们理解其内部工作。### 6.1 工作集Working Set每个进程都有一个“工作集”即该进程当前在物理内存中或被认为在活跃使用中的页面集合。工作集管理器会根据系统的内存压力、页面的访问频率A位和脏状态D位动态调整工作集大小将不活跃的页面移出修剪工作集必要时写回磁盘如果是脏页。当进程再次访问一个已被移出工作集的页面时会触发硬缺页Hard Page Fault需要从磁盘页面文件或映射文件读回速度很慢。### 6.2 原型PTE与共享页面为了实现共享内存如DLL和写时复制Windows引入了“原型PTE”Prototype PTE的概念。对于共享页面进程私有的PTE并不直接指向物理页面而是指向一个原型PTE结构。原型PTE再指向真正的物理页面。这样当需要修改共享页面触发写时复制时只需修改进程私有的PTE指向一个新的物理页副本而不会影响其他进程。这优化了共享内存的管理。### 6.3 地址窗口化扩展AWE与物理地址扩展PAE虽然x64已经拥有巨大的地址空间但早期x86的32位系统只有4GB虚拟地址空间。为了访问更多物理内存Intel引入了PAEPhysical Address Extension模式将物理地址从32位扩展到36位最大64GB。PAE模式将页表项从32位扩展到64位并引入了第三级页表PDPT形成了类似“2-9-9-12”的分页结构。AWEAddress Windowing Extensions是Windows提供的一套API允许32位应用程序通过一个“窗口”来轮流映射和访问大于4GB的物理内存。在x64环境下由于虚拟地址空间本身已经足够大AWE的使用场景减少了但其背后的思想——通过重映射窗口来访问大内存——在某些特定场景下仍有价值。7. 调试与故障排查中的分页知识应用当系统出现内存相关的蓝屏如IRQL_NOT_LESS_OR_EQUAL,PAGE_FAULT_IN_NONPAGED_AREA,SYSTEM_SERVICE_EXCEPTION时分页知识是定位问题的关键。### 7.1 分析蓝屏转储以PAGE_FAULT_IN_NONPAGED_AREA (50)为例错误参数中会给出导致缺页的虚拟地址。你的第一反应应该是这个地址是用户态地址还是内核态地址高16位是0还是全1它可能是什么是空指针、野指针还是已经被释放的池Pool地址使用!pte 地址查看该地址的页表项。如果PTE为0或P位为0说明它没有被有效映射。如果U/S位显示为用户态但错误发生在内核态IRQL APC_LEVEL那很可能是一个驱动错误地引用了用户态地址而没有进行探测Probe。### 7.2 使用!pte和!pool命令结合使用这些命令非常有效。例如如果崩溃地址看起来像一个池标签Pool Tag可以用!pool 地址来验证它是否是一个有效的池分配以及它属于分页池还是非分页池。如果驱动在DISPATCH_LEVEL或更高IRQL下访问了一个分页池地址就会导致蓝屏。### 7.3 理解“非分页”的含义“非分页”不仅仅意味着页面不会被换出到磁盘。在更高的中断请求级别IRQL DISPATCH_LEVEL处理器不允许产生缺页异常因为缺页异常处理程序本身可能需要被分页的代码和数据这会导致死锁。因此任何在DISPATCH_LEVEL或更高IRQL下运行的代码包括它访问的数据都必须位于非分页内存中。这是驱动开发中最容易触犯的规则之一。我在排查一个第三方存储驱动的问题时就遇到过类似情况。驱动在一个DPC例程IRQL DISPATCH_LEVEL中通过一个指针访问了数据。大部分时间这个指针指向非分页内存所以运行正常。但在极少数情况下由于驱动内部的逻辑错误该指针指向了一个之前从分页池分配、后来已被释放并可能被重用的内存块。当这个被重用的块恰好不在物理内存中时访问它就触发了缺页而由于IRQL过高系统无法处理直接蓝屏。使用!pte查看崩溃地址发现其PTE的P位为0并且!pool命令显示该地址不属于任何活跃的池分配最终将问题定位到驱动对某个指针的生命周期管理有误。理解9-9-9-9-12分页不仅仅是记住一个地址转换公式。它是理解现代操作系统内存管理、系统稳定性、安全性和性能优化的钥匙。从硬件的页表遍历到操作系统的缺页处理、工作集管理、共享内存再到驱动开发中的内存操作规范这条线索贯穿始终。下次当你面对一个神秘的内存访问错误时希望你能想起这四级页表并知道该从哪里开始你的侦探工作。
返回列表