Windows

分页机制(10-10-12)

参考文章:https://github.com/0voice/kernel_memory_management/blob/main/%E2%9C%8D%20%E6%96%87%E7%AB%A0/%E4%B8%80%E6%96%87%E5%B8%A6%E4%BD%A0%E4%BA%86%E8%A7%A3%EF%BC%8C%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E3%80%81%E5%86%85%E5%AD%98%E5%88%86%E9%A1%B5%E3%80%81%E5%88%86%E6%AE%B5%E3%80%81%E6%AE%B5%E9%A1%B5%E5%BC%8F%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md

为什么要用分页机制?

因为分段机制效率低,是为了解决分段粒度大,因为段需要整段的加载进内存以及整段换出,造成内存碎片大,不易于管理,虽然可以通过将段置换出磁盘再加载的方式减少碎片,但是效率实在太低

分页管理通过划分物理空间为一块块固定大小的页与之对应,能够将程序分割成一页一页加载进内存,提升了内存的利用率

所以引入了分页机制. 分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段, 藉此解决因段长度不同而带来的内存空间管理问题.

分页机制工作原理

控制寄存器

CR(Control Register)控制寄存器,这些寄存器只能在0环使用

寄存器名称 描述
CR0 包含处理器标志控制位,例如PE,PG,WP等
CR1 保留
CR2 专门用于保存缺页异常时的线性地址
CR3 保存进程页目录地址,切换进程用
CR4 扩展功能(如判断物理地址扩展模式等),Pentium系列(包括486的后期版本)处理器中才实现

CR0寄存器:

1722081811572

PE:启用保护模式标志,1是保护模式,0是实模式,这个位只是开始或关闭段机制,并没有启用分页机制。

PG:分页机制开关,在启用之前需要确保PE是开启的,否则会出现异常。

WP:写保护标志,禁止0环程序向3环只读页面执行写操作,也就是说当CPL<3的时候,如果WP=0,可以读写任意物理页,只要线性地址有效,如果WP=1,可以读取任意用户级物理页,但是对于只读的物理页,不能写 。

CR4寄存器

1722082069659

PAE (Physical Address Extension,物理地址扩展):
启用PAE可以使处理器支持超过4GB的物理内存寻址能力,增加到36位地址总线,从而支持高达64GB的物理内存。PAE=1开启
启用PAE后,页表结构会发生变化,使用三级页表:页目录指针表(PDP)、页目录(PD)和页表(PT)。
在启动选项 /nopae /pae 分别是关闭和开启PAE标志的指令

PSE (Page Size Extension,页大小扩展) :
PSE标志位是CR4寄存器的第4位(CR4.PSE)。启用PSE可以使处理器支持4MB的大页,从而减少页表开销,提高内存管理效率。 启用PSE后,页表结构中的页目录项(PDE)可以指向一个4MB的大页,而不是传统的4KB小页。

1722082317882

这个表列举了标志位组合起来的情况,(PS(PDE)这个属于页表的标志位)

做表映射物理地址:

这个表要存一个物理地址(32bit)+在这个物理地址的分页的属性(32bit),所以一个表就是64bit

那么这个需要多少个这样的表?很好计算,32bit所代表的地址单元为2^32次方,一个页能代表的地址单元为4k,除一下就可以知道是1,048,576个表

那个所有表占的内存大小就是1048576*8字节(64bit),即为0x800000,也就是8M,也就是一个进程就要给它的页表就要8M

如何优化这个结构让它缩小??

就是这个表可以被进行压缩,因为一个页是0x1000的大小,这个是固定的,那么低12bit其实可以被舍弃,反正到时候再加上虚拟地址低12bit偏移也是个一样的,那么地址就变成了20bit,然后属性再压缩一下变成12bit,这样一个表就变成了32bit,大小变成了4M

二级页表(4KB)

所以如果一个分页是4KB,那么就有两个表(那个物理地址不是表,实际上是俩表)

1722081432425

一个叫页目录,一个叫页表

页目录 (Page Directory) :

页目录存放的是页表的地址。它充当一个索引,指向多个页表。

每个页目录条目(Page Directory Entry,PDE)包含一个页表的物理地址。

一个页目录通常可以包含许多页目录条目,每个条目指向一个页表。

页目录表格式:

1722155863832

高20位是页首地址

**页表基地址(Page Table Base Address) [31:12]**:

  • 页表的物理地址的高20位。页表的物理地址通常是4KB对齐的,因此低12位为0。(如果PS为1,那么一个分页就是4M,那么这里直接是物理首地址,如果分页是4K,那么是物理页首地址)

**保留(Reserved) [11:9]**:

  • 这些位是保留位,具体使用情况依赖于特定的处理器实现,一般不用。

**G (Global) [8]**:

  • 全局页指示位。如果设置为1,表示这个页对于所有的进程都是全局的,不会被TLB刷新影响。

**PS (Page Size) [7]**:

  • 页大小指示位。如果设置为0,表示页大小为4KB;如果设置为1,表示页大小为4MB。

**A (Accessed) [6]**:

  • 访问位。处理器设置这个位以指示该页目录条目已被访问过。

**0 [5]**:

  • 必须为0。在某些特定情况下被使用。

**U/S (User/Supervisor) [4]**:

  • 用户/超级用户位。如果设置为0,则只能在超级用户模式下访问;如果设置为1,则可以在用户模式下访问。(有页表则该标志位没啥用,属性由页表描述)

**R/W (Read/Write) [3]**:

  • 读/写位。如果设置为0,则该页是可读可执行的;如果设置为1,则该页是可读写可执行的。有页表则该标志位没啥用,属性由页表表示)

**P (Present) [0]**:

  • 存在位。如果设置为1,表示页在物理内存中;如果设置为0,表示页不在物理内存中(例如在硬盘上)。

页表(Page Table)

页表存放的是页框(Page Frame)的地址。它充当一个索引,指向实际的物理内存。

每个页表条目(Page Table Entry,PTE)包含一个物理页框的地址。

一个页表通常可以包含许多页表条目,每个条目指向一个物理内存页框。

1722156510604

区别就是PS标志位不需要了,因为已经在页目录表 表示过了

其他标志位和页目录表是一样的

从线性地址转到物理地址的流程:

  1. 线性地址分解
    • 线性地址:LA = 31-22 (PD) | 21-12 (PT) | 11-0 (Offset)
  2. 页目录基址
    • CR3寄存器保存页目录的基地址(页目录基址)。
  3. 页目录项
    • 从CR3寄存器中获取页目录基地址,将线性地址的高10位(31-22)与页目录基地址相加,得到页目录项的物理地址。
    • 页目录项包含指向页表的基地址。
  4. 页表项
    • 使用页目录项提供的页表基地址,加上线性地址的中间10位(21-12),得到页表项的物理地址。
    • 页表项包含指向实际页框(物理页)的基地址。
  5. 页内偏移
    • 使用页表项提供的页框基地址,加上线性地址的低12位(11-0),得到最终的物理地址。

windbg中 d指令是查看虚拟内存,dd是查看物理内存

db查看虚拟内存,!db查看物理内存

多级页表的优势:

对于单页表的实现方式,在 32 位和页大小 4KB 的环境下,一个进程的页表需要装下 100 多万个「页表项」,并且每个页表项是占用 4 字节大小的,于是相当于每个页表需占用 4MB 大小的空间。

我们把这个 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 1024 个页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成二级分页。如下图所示:

1722087590628

分了二级表,映射 4GB 地址空间就需要 4KB(一级页表)+ 4MB(二级页表)的内存,这样占用空间不是更大了吗?

当然如果 4GB 的虚拟地址全部都映射到了物理内上的,二级分页占用空间确实是更大了,但是,我们往往不会为一个进程分配那么多内存。

其实我们应该换个角度来看问题,还记得计算机组成原理里面无处不在的局部性原理么?

每个进程都有 4GB 的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到 4GB,因为会存在部分对应的页表项都是空的,根本没有分配,对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。

如果使用了二级分页,一级页表就可以覆盖整个 4GB 虚拟地址空间,但如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB

,这对比单级页表的 4MB 是不是一个巨大的节约?

那么为什么不分级的页表就做不到这样节约内存呢?我们从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。

操作系统如何切换线程?

操作系统将即将运行的进程的页目录的物理地址加载到CR3寄存器。CR3寄存器的值被更新为新进程的页目录基地址。

三环访问0地址

通过修改页表,可以让三环访问0地址

(淦,我的Win都是2-9-9-12分页机制,还没学,后续补上)

通过API修改物理内存

给一个物理地址,可以返回一个虚拟地址进行操作。用API叫做MmMapIoSpace,原理就是找一个空的页表,建立映射关系

1
2
3
4
5
PVOID MmMapIoSpace(
PHYSICAL_ADDRESS PhysicalAddress,
SIZE_T NumberOfBytes,
MEMORY_CACHING_TYPE CacheType
);

参数

  1. PhysicalAddress:
    • 类型: PHYSICAL_ADDRESS
    • 描述: 设备的物理地址,需要映射到虚拟地址空间。
  2. NumberOfBytes:
    • 类型: SIZE_T
    • 描述: 要映射的字节数。
  3. CacheType:
    • 类型: MEMORY_CACHING_TYPE
    • 描述: 指定映射的缓存属性。常用的值有:
      • MmNonCached: 不缓存。
      • MmCached: 缓存。
      • MmWriteCombined: 写合并。
      • MmHardwareCoherentCached: 硬件一致性缓存(特定平台)。

返回值

  • 返回值为 PVOID,表示映射到虚拟地址空间的基地址。
  • 如果映射失败,返回 NULL

还有一个方法:

使用ZwOpenSection,ZwMapViewOfSection,ZwUnmapViewOfSection

打开Winobj.exe,在设备那里可以找到物理内存条,Type是Section,与Device有区别:

Device 在 Windows 内核中通常指的是一个设备对象(DEVICE_OBJECT),它是用来表示硬件设备的抽象。设备对象是驱动程序与操作系统及其应用程序之间的接口。

Section 在 Windows 内核中通常指的是一个节对象(SECTION_OBJECT),它是用来表示内存中的一段连续区域,可以映射到进程的虚拟地址空间。节对象通常用于内存映射文件和共享内存。

总之:Device: 设备对象用于表示硬件设备,主要用于设备驱动程序中的 I/O 操作和驱动程序通信。

Section: 节对象用于表示内存中的一段区域,主要用于内存映射文件和共享内存。

设备对象侧重于与硬件设备的交互,而节对象侧重于内存管理和文件映射。

1722263462801

下节预告:

2-9-9-12机制,PAE机制,一个32位程序可以操作36位内存

如何做?

在CPU加了4根线,再通过改进多级页表(2-9-9-12机制),这样就能映射64G内存大小