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 | 软件可用位,操作系统自定义用途 |
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 策略 |
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 的代码均可访问。内核态可以访问用户态页面,但用户态不能访问内核态页面。
这种设计确保了内核代码、内核数据、页表本身等关键结构对用户态程序完全不可见,是实现操作系统安全隔离的基石。
4.4 权限检查流程
当 CPU 执行一条内存访问指令(如 MOV、CALL、JMP)时,MMU 在地址翻译过程中同步进行权限检查。以下是完整的检查流程:
- 获取 CPL:从当前代码段寄存器 CS 的低 2 位获取当前特权级。
- 检查 Present 位:如果 PTE 的 P 位为 0,直接触发缺页异常(#PF),不继续检查。
- 检查 U/S 位:如果 CPL=3 且 U/S=0,触发页面错误(违规访问内核页)。
- 检查访问类型:
- 如果是写操作:检查 R/W 位。如果 R/W=0,触发页面错误(只读页写入)。
- 如果是取指操作:检查 NX 位。如果 NX=1,触发页面错误(不可执行页取指)。
- 设置 A/D 位:如果检查通过,硬件自动将 A 位置 1;如果是写操作,还将 D 位置 1。
- 完成翻译:使用 PTE 中的物理页帧号(PFN)拼接偏移量,得到最终物理地址。
下图展示了 MMU 权限检查的完整决策流程:
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) | 攻击者修改内核代码逻辑,植入后门 |
| 权限提升 | 用户态页表被映射为内核可写 | 攻击者修改用户态页表,获得对内核空间的访问 |