4. PTE 权限位解析:读/写/执行权限(R/W/X)、用户态/内核态隔离(U/S 位)、权限检查流程

页表项(PTE)中的权限位是内存保护的第一道防线。操作系统通过设置这些位,精确控制每个物理页帧的访问模式,并严格隔离用户态与内核态的内存空间。本节将深入解析 R/W/X 权限位、U/S 特权级位,以及硬件 MMU 执行权限检查的完整流程。

4.1 权限位布局与语义

在 x86-64 架构的 64 位 PTE 中,权限相关的位集中在低 12 位(属性位)中。下表列出了核心权限位的名称、位置与功能:

位域 名称 功能描述
Bit 0 P (Present) 页是否存在物理内存中;为 0 时触发缺页异常
Bit 1 R/W (Read/Write) 0 = 只读;1 = 可读可写
Bit 2 U/S (User/Supervisor) 0 = 仅内核态可访问;1 = 用户态和内核态均可访问
Bit 3 PWT (Page Write-Through) 缓存写策略:直写或回写
Bit 4 PCD (Page Cache Disable) 禁止该页的缓存
Bit 5 A (Accessed) 硬件置位,表示页被访问过
Bit 6 D (Dirty) 硬件置位,表示页被写入过
Bit 7 PS (Page Size) 页大小标志(大页支持)
Bit 8 G (Global) 全局页,TLB 刷新时不被清除
Bits 9-11 Ignored/AVL 软件可用位,操作系统自定义用途
关键认知: 权限位是硬件 MMU 直接解析的,操作系统只能通过设置这些位来间接控制访问。任何违反权限的访问都会由 MMU 触发页面错误异常,操作系统在异常处理程序中进行后续处理。

4.2 读/写/执行权限(R/W/X)

虽然 x86-64 的 PTE 中只有 R/W 位(Bit 1)直接控制读写权限,但执行权限由另一个机制——NX(No-Execute)位控制。NX 位位于 PTE 的 Bit 63(在 PAE 模式下为 Bit 63),当该位为 1 时,该页不可执行代码。

下表总结了 R/W 位与 NX 位组合形成的四种权限模式:

R/W 位 NX 位 权限模式 典型用途
0 0 只读 + 可执行 代码段(.text),可读可执行但不可写
0 1 只读 + 不可执行 只读数据段(.rodata),防止代码注入
1 0 读写 + 可执行 JIT 编译器生成的代码页(极少见,高风险)
1 1 读写 + 不可执行 数据段(.data, .bss)、堆、栈,标准 W^X 策略
安全实践:W^X 策略 现代操作系统严格遵循 W^X(Write XOR Execute)原则:任何内存页要么可写,要么可执行,但不能同时具备两种权限。这有效防止了缓冲区溢出攻击中将 shellcode 写入数据区并执行的经典手法。

4.3 用户态/内核态隔离(U/S 位)

U/S 位(Bit 2)实现了最基础的特权级隔离。CPU 当前运行在 CPL(Current Privilege Level) 下,CPL=0 为内核态,CPL=3 为用户态。MMU 在地址翻译时比较 CPL 与 PTE 的 U/S 位:

  • U/S = 0(仅内核态):只有 CPL=0 的代码可以访问。用户态代码访问该页会触发页面错误。
  • U/S = 1(用户态+内核态):CPL=0 和 CPL=3 的代码均可访问。内核态可以访问用户态页面,但用户态不能访问内核态页面。

这种设计确保了内核代码、内核数据、页表本身等关键结构对用户态程序完全不可见,是实现操作系统安全隔离的基石。

隔离效果: 用户态进程无法读取或修改内核态页面的任何内容,即使知道物理地址也无法绕过 MMU 的权限检查。所有跨特权级的数据传递必须通过操作系统提供的系统调用接口,由内核在受控条件下进行数据拷贝。

4.4 权限检查流程

当 CPU 执行一条内存访问指令(如 MOV、CALL、JMP)时,MMU 在地址翻译过程中同步进行权限检查。以下是完整的检查流程:

  1. 获取 CPL:从当前代码段寄存器 CS 的低 2 位获取当前特权级。
  2. 检查 Present 位:如果 PTE 的 P 位为 0,直接触发缺页异常(#PF),不继续检查。
  3. 检查 U/S 位:如果 CPL=3 且 U/S=0,触发页面错误(违规访问内核页)。
  4. 检查访问类型:
    • 如果是写操作:检查 R/W 位。如果 R/W=0,触发页面错误(只读页写入)。
    • 如果是取指操作:检查 NX 位。如果 NX=1,触发页面错误(不可执行页取指)。
  5. 设置 A/D 位:如果检查通过,硬件自动将 A 位置 1;如果是写操作,还将 D 位置 1。
  6. 完成翻译:使用 PTE 中的物理页帧号(PFN)拼接偏移量,得到最终物理地址。

下图展示了 MMU 权限检查的完整决策流程:

MMU 权限检查流程 MMU 权限检查流程 内存访问请求 步骤1:获取 CPL(当前特权级) 步骤2:检查 P 位(Present) P=0 触发缺页异常 步骤3:检查 U/S 位 违规 触发页面错误 步骤4:检查访问类型(读/写/取指) 写操作 检查 R/W 位 取指操作 检查 NX 位 读操作 步骤5:硬件置位 A/D 位 翻译完成,访问物理地址

4.5 权限检查的硬件加速与软件协同

MMU 的权限检查完全由硬件实现,无需软件干预,这是其高效性的关键。但操作系统需要与硬件协同完成以下工作:

  • 页表构建:操作系统在创建进程时,根据内存布局(代码段、数据段、堆、栈、内核映射等)正确设置每个 PTE 的 R/W、U/S、NX 位。
  • 异常处理:当 MMU 触发页面错误时,操作系统异常处理程序分析错误码(包含违规类型:读/写/执行、用户态/内核态、缺页/权限违规),决定是修复映射(如按需调页)还是杀死违规进程。
  • TLB 一致性:当操作系统修改 PTE 权限位后,必须执行 TLB 刷新指令(如 invlpg),确保 MMU 使用最新的权限设置。
性能优化: 由于权限检查在硬件流水线中完成,与地址翻译并行执行,因此对正常内存访问几乎无性能开销。只有在触发页面错误时,才会陷入操作系统进行慢速处理。

4.6 权限位配置实战示例

以下代码展示了 Linux 内核在建立页表时如何设置权限位。以 x86-64 架构的 set_pte_at 函数为例:

// Linux 内核源码片段:设置页表项权限
static inline void set_pte_at(struct mm_struct *mm, unsigned long addr,
                              pte_t *ptep, pte_t pte)
{
    // 根据 vma 的权限标志构建 PTE 权限位
    // vma->vm_flags 包含 VM_READ, VM_WRITE, VM_EXEC, VM_USER 等
    
    pte_t new_pte = pte;
    
    // 设置 Present 位
    new_pte = pte_set_flags(new_pte, _PAGE_PRESENT);
    
    // 设置 R/W 位:只有 VM_WRITE 时才允许写入
    if (vma->vm_flags & VM_WRITE)
        new_pte = pte_set_flags(new_pte, _PAGE_RW);
    else
        new_pte = pte_clear_flags(new_pte, _PAGE_RW);
    
    // 设置 U/S 位:用户态映射设置 _PAGE_USER
    if (vma->vm_flags & VM_USER)
        new_pte = pte_set_flags(new_pte, _PAGE_USER);
    else
        new_pte = pte_clear_flags(new_pte, _PAGE_USER);
    
    // 设置 NX 位:不可执行时设置 _PAGE_NX
    if (!(vma->vm_flags & VM_EXEC))
        new_pte = pte_set_flags(new_pte, _PAGE_NX);
    else
        new_pte = pte_clear_flags(new_pte, _PAGE_NX);
    
    // 写入页表
    set_pte(ptep, new_pte);
    
    // 刷新 TLB(如果需要)
    if (pte_write(new_pte) || pte_exec(new_pte))
        flush_tlb_page(vma, addr);
}
实践要点: 在编写操作系统或驱动代码时,务必遵循最小权限原则——只为内存页分配其功能所需的最小权限集。例如,代码段只给读+执行权限,数据段只给读+写权限,内核数据结构只给内核态访问权限。

4.7 权限位与安全攻击面

PTE 权限位的错误配置是内核安全漏洞的常见来源。以下是几种典型攻击场景:

攻击类型 错误配置 攻击效果
代码注入 数据页设置了可执行权限(NX=0) 攻击者将 shellcode 写入缓冲区后直接跳转执行
内核数据泄露 内核页错误地设置了 U/S=1 用户态进程直接读取内核敏感数据(如密码哈希)
内核代码修改 内核代码段设置了可写权限(R/W=1) 攻击者修改内核代码逻辑,植入后门
权限提升 用户态页表被映射为内核可写 攻击者修改用户态页表,获得对内核空间的访问
防御建议: 定期使用内核安全扫描工具(如 KASLR、SMAP/SMEP、PTI)检查页表权限配置。在代码审查中,重点关注页表创建和修改路径,确保权限位设置符合预期。