页表及页起始VA的解析

        页表,连接虚拟内存和物理内存的一个重要机制,在搞清页表是什么东西之前,我们需要先区分物理内存和虚拟内存。    

        我们都知道运行内存和磁盘不是一个东西,但是都是用来存储数据的东西,同时进程运行的虚拟地址空间也是用来存储数据的东西,那么这些东西有哪些区别呢,以下我们从硬件入手来讲解虚拟内存和物理内存的区别:

虚拟内存和物理内存

RAM:随机地址存储器,分为SRAM和DRAM

SRAM(静态存储器)读取速度快,一般适用于Cache缓存价格高
DRAM(动态存储器)读取速度稍慢,用于运行内存价格偏低

SSD:固态硬盘,用于磁盘,存储大容量数据

Cache:CPU的缓存,FIFO结构,存储CPU频繁使用的数据/指令

主存(运行内存,物理内存)+ 硬盘 = 虚拟内存实现(容量大,可随机访问)

虚拟内存是逻辑地址空间,虽然有对应的物理内存,但是“看”上去更大

Swap分区:操作系统在磁盘上预留的一块空间,用来在物理内存不够时,或者将不常用的内存页放入磁盘的swap分区(页面置换)

+-------------------+       +------------------+
|      RAM          |       |    Swap 分区      |
|  常用数据/代码     | <-->  |   不常用页       |
+-------------------+       +------------------+

下面是虚拟内存和物理内存的逻辑转换图:

虚拟地址--CR3-->主存<--页交换-->磁盘

虚拟地址通过CPU中的CR3寄存器中存储的页顶指针来访问对应的物理内存地址,当物理内存不够使用时,通过页交换机制来将不常用的页放置到磁盘中的swap分区,需要时再取回

页表Page Table

基本概念

维护虚拟地址到物理地址的映射关系,可以结合MMU和TLB实现虚拟化和保护。

为什么需要页表:CPU不能直接访问操作物理内存,必须通过虚拟内存(通过内存管理单元MMU将其转化为对应的物理地址)才能操作物理内存,转换过程依赖于页表

定义的概念如下:

页(Page):虚拟内存和物理内存被分成固定大小的块,通常是 4KB

页框(Frame):物理内存中的页

页表(Page Table):记录每个虚拟页对应的物理页框(Frame)的地址

页表是一张映射表,把虚拟页号(VPN, Virtual Page Number)映射到物理页号(PFN, Page Frame Number)

页表又区分为用户态页表和内核态页表

  • 用户态页表:每个用户进程独立,映射用户空间的虚拟地址

  • 内核态页表:所有进程共享一部分,用于映射内核空间地址(每个进程页表中都存在,但映射相同的物理内存)

页表的分级存储机制

页表同时也是分级存储:按照CR4中的寄存器标志位,页表最多有五级页表(不论层级多高,只有最后面一层是真正记录的页表项Page Table Entry,前面的都是目录)

x86 32位:二级页表(页目录 + 页表)

x86_64:四级页表(PML4 → PDPT → PD → PT),64位只有48位有效位,被拆分为5个部分,每一级页表可以存放2^9 = 512个条目

[ 9位 PML4 | 9位 PDPT | 9位 Page Directory | 9位 Page Table | 12位 页内偏移 ]
PML4(Page Map Level 4):顶层页表,CR3 保存的就是它的物理地址,每个PML4E(条目)指向一个 PDPT 页
PDPT(Page Directory Pointer Table):指向一个 Page Directory 页
Page Directory(页目录,PD):指向一个 Page Table 页
Page Table(页表,PT):最底层,存放PTE,每个PTE映射一个4KB物理页

本身存储在物理内存中,每个进程都有自己独立的页表,操作系统切换进程时,就是切换页表。页表本身就是一堆内存(里面存放的是虚拟页号 → 物理页框号的映射),本身不和进程代码、数据放在一起。虽然存储位置不相同,但是进程和页表是强绑定关系。我们一般通过EPROCESS->DirectoryTableBase访问CR3的值

页表的相关机制

延迟绑定

使用多级页表,只有被使用到的部分虚拟地址才需要真正分配下一级表,没有使用的只在该级中占个位置即可

共享机制

使用共享内存时(内存映射或者共享),它们的页表中会有不同的虚拟地址映射到相同的物理页

TLB

后备缓冲器,CPU 内部的一个高速缓存,用来 缓存页表的部分内容,存放常用的VPN->PFN的映射表,这样就不用每次访问4级页表进行4次内存访问了。可以理解为页表缓存,TLB存储在MMU中

MMU

页表解析器,用于遍历页表从页顶指针遍历读取到最底层的Table Entry结构

利用CR3找到对应的物理地址

MDL

内存描述列表,用于描述一段虚拟内存对应的物理页面。将一段虚拟地址拆解为底层的物理页数组,供驱动,DMA使用。当内核想锁定一段虚拟内存,避免被换出时(比如文件 I/O、网络传输),需要先创建 MDL

MDL 是内核在页表的基础上,抽取并缓存一段内存的映射信息,方便后续操作。

分页机制(Paging)

把 虚拟内存和物理内存划分为大小相同的块来建立映射,虚拟地址空间划分为页,物理内存划分为页框,单位都是4KB,而页表就是记录页号到页框号的映射,实现进程隔离和内存保护(通过页表的权限位控制访问)

缺页异常(Page Fault)

异常处理机制,触发缺页中断,当访问的虚拟页没有对应的物理页时触发异常,然后操作系统检查该地址有否有效,有效则将虚拟页调入物理内存,并更新页表

页交换(Paging)

分为页换出(Page Out)和页换入(Page In)。页换出是操作系统在内存不足时,将当前未活跃或优先级较低的页从物理内存中移出到磁盘的swap分区中;页换入是触发缺页异常时,重新将页从硬盘读回来,分配新的页框。同时结合换页算法FIFO,LRU(优先换出最近没用的页)等

物理内存中的常驻内存

对于一些重要的数据和结构体,会始终存储在物理内存中不会被换出,如果换出会导致系统的崩溃,以下列举一些

1. 内核代码与内核数据结构

  • 内核代码段(text):操作系统内核本身的执行代码。

  • 内核数据段(data/bss):包括全局变量、静态变量等。

  • 内核堆(kernel heap):如通过 kmalloc() 分配的内存(Linux)。

  • 页表(Page Tables):用于虚拟地址到物理地址的映射。

  • 中断描述符表(IDT)全局描述符表(GDT) 等 CPU 使用的关键结构。

2. 内核栈(Kernel Stack)

  • 每个进程在内核态都有一个内核栈(通常 8KB 或 16KB)。

  • 用于处理系统调用、中断等。

  • 不会换出,因为换出会导致内核无法执行。

Windbg调试页表

接下来,我使用Windbg调试Win7虚拟机中的32位程序

由于页表是进程私有结构,先切换到进程虚拟地址空间中

3: kd> ! process 0 0
    PROCESS 88226030  SessionId: 1  Cid: 09c8    Peb: 7ffd8000  ParentCid: 0940
    DirBase: be66c560  ObjectTable: abad6818  HandleCount:  62.
    Image: notepad.exe
//其实此处的DirBase就是指向页表的位置,但是是物理位置,我们无法访问,必须要得到虚拟地址映射
        
//切换到notepad程序的虚拟地址空间中
3: kd> .process /r /p 88226030
ReadVirtual: 88226048 not properly sign extended
Implicit process is now 88226030
.cache forcedecodeuser done
Loading User Symbols

通过左掩码(Left Mask)与虚拟内存来获得每个页的起始位置,然后使用指令查看

3: kd> !pte 0x7FFDF000
VA 7ffdf000                     //此处的虚拟地址
PDE at C0601FF8                 //此处为一级页表条目(或者叫二级页表)
PTE at C03FFEF8                 //对应的最终页表条目(最终的物理页框)
contains 000000005C8B4867       //PDE存储的数据(按二进制位解析)
contains 0000000000000000       //PTE存储的数据(0表示没有对应的页框)
pfn 5c8b4                       //指向物理页框的编号是5c8b4
---DA--UWEV                     //每一个字母代表一个标志(脏页,用户模式,可写可执行)
not valid                       //该虚拟页目前没有映射到物理内存

这样我们就获得了页的基本属性和对应物理页的编号

手动解析页表结构

我们对一个VA进行地址解析

//64位只使用48位,前12位保留
| PML4 | PDPT | PD | PT | Offset |
| 9b   | 9b   | 9b | 9b | 12b    |
    
 //通过VA计算
 给定虚拟地址 VA:
PML4 index = (VA >> 39) & 0x1FF → 从 PML4 表找到 PDPT 基址
PDPT index = (VA >> 30) & 0x1FF → 从 PDPT 表找到 PD 基址
PD index = (VA >> 21) & 0x1FF → 从 PD 表找到 PT 基址或大页
PT index = (VA >> 12) & 0x1FF → 从 PT 表找到物理页基址
Offset = VA & 0xFFF → 页内偏移

由于页表是分层结构,我们需要先了解每一层级对应的数据是什么:

层级英文全称缩写位范围描述每条大小
顶级Page Map Level 4PML4VA[47:39]顶级页表,指向 PDPT(Page Directory Pointer Table)8字节/条目,512条
第二级Page Directory Pointer TablePDPTVA[38:30]指向 Page Directory(PD),每条可能映射 1GB大页8字节/条目,512条
第三级Page DirectoryPDVA[29:21]指向 Page Table(PT),每条可能映射 2MB大页8字节/条目,512条
第四级Page TablePTVA[20:12]指向实际物理页(4KB)8字节/条目,512条
页内偏移OffsetVA[11:0]页内偏移4KB 页

通过大页管理小页的机制来从1GB->2MB->4KB的页管理(PML4E->PDPT->PDE->PTE)

typedef union _PDE_64 {
    UINT64 Value;
    struct {
        UINT64 Present           : 1;  // 位0
        UINT64 ReadWrite         : 1;  // 位1
        UINT64 UserSupervisor    : 1;  // 位2
        UINT64 PageWriteThrough  : 1;  // 位3
        UINT64 PageCacheDisable  : 1;  // 位4
        UINT64 Accessed          : 1;  // 位5
        UINT64 Dirty             : 1;  // 位6,只有PageSize=1才有意义
        UINT64 PageSize          : 1;  // 位7,1=大页(2MB或1GB)
        UINT64 Global            : 1;  // 位8,只有PageSize=1才有意义
        UINT64 Available         : 3;  // 位9-11
        UINT64 PageFrameNumber   : 40; // 位12-51
        UINT64 Reserved          : 11; // 位52-62
        UINT64 Nx                : 1;  // 位63
    } Bits;
} PDE_64, *PPDE_64;

typedef union _PTE_64 {
    UINT64 Value;
    struct {
        UINT64 Present           : 1;  // 位0,页是否有效
        UINT64 ReadWrite         : 1;  // 位1,读写权限,1=可写
        UINT64 UserSupervisor    : 1;  // 位2,用户/内核权限,1=用户可访问
        UINT64 PageWriteThrough  : 1;  // 位3,缓存策略
        UINT64 PageCacheDisable  : 1;  // 位4,缓存禁用
        UINT64 Accessed          : 1;  // 位5,CPU自动置位,页被访问过
        UINT64 Dirty             : 1;  // 位6,CPU写入时置位
        UINT64 PageSize          : 1;  // 位7,页大小,PT级固定为0
        UINT64 Global            : 1;  // 位8,全局页
        UINT64 Available         : 3;  // 位9-11,操作系统可用
        UINT64 PageFrameNumber   : 40; // 位12-51,物理页帧号
        UINT64 Reserved          : 11; // 位52-62,保留
        UINT64 Nx                : 1;  // 位63,执行禁止
    } Bits;
} PTE_64, *PPTE_64;

通过上述两个层级结构体的定义,可以看出,PTE和PDE的结构定义基本相似,但是不同的是PDE 可以直接映射大页(PS=1),而且都是按二进制位来解析数据

虚拟内存和页表设计的核心

每一个页起始位置都有含义(按照二进制位来解析可知)

每一个页的大小固定:

  • 小页(4KB) → 页内偏移 12 位

  • 大页(2MB / 1GB) → 页内偏移更多(2MB页地址低21位为0)

通过上面对VA的分析可知,每一个页面的起始位置都包含着对应物理页的信息,一一对应(CPU通过虚拟地址的高位计算页表索引,低位偏移得到页内偏移)

简单说一下计算原理:

比如64位的页基址0x000000007FFDF123

VA = 0x000000007FFDF123
页内偏移0x123
PML4 index  = (VA >> 39) & 0x1FF
PDPT index  = (VA >> 30) & 0x1FF
PD index    = (VA >> 21) & 0x1FF
PT index    = (VA >> 12) & 0x1FF

这样所有的索引其实都存储在页的起始位置,所以MMU才可以通过CR3的位置往下一路寻址到PTE。

总结

        页表就是一个数据结构,存储着物理内存到虚拟内存的映射,使用到的物理内存就在页表中有实际的数据,没有使用到的空间先在页表中占位,等有数据需要存储到对应位置时,再分配对应的物理页框,这样就可以使虚拟内存“看”上去很大,也是一个16G运行内存的内存条不止可以跑4个32位程序一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值