linux内核页表与用户态页表

张开发
2026/4/14 14:10:22 15 分钟阅读

分享文章

linux内核页表与用户态页表
页表基础页表是虚拟地址VA→物理地址PA的映射表由多级目录组成x86_64 典型 4 级PGD → PUD → PMD → PTEPGDPage Global Directory页全局目录顶级PUDPage Upper Directory页上级目录PMDPage Middle Directory页中间目录PTEPage Table Entry页表项最终指向物理页帧地址转换4KB 页虚拟地址 PGD[47:39] PUD[38:30] PMD[29:21] PTE[20:12] 页内偏移[11:0]64 位虚拟地址空间划分x86_64 Linux用户空间0x0000_0000_0000_0000 ~ 0x0000_7fff_ffff_ffff128TB内核空间0xffff_8000_0000_0000 ~ 0xffff_ffff_ffff_ffff128TB内核页表Kernel Page Table定义系统全局唯一的页表内核自身使用所有进程共享。根目录swapper_pg_dirinit_mm.pgd生命周期系统启动 → 关机核心映射直接映射区线性映射PAGE_OFFSET开始VA PA PAGE_OFFSETvmalloc 区非连续虚拟地址动态映射固定映射区fixmap编译时固定虚拟地址模块区、PCI/MMIO 映射特性权限U/S0仅内核态可访问稳定性核心代码 / 数据常驻内存不换出缺页内核缺页如 vmalloc 未映射→ 内核处理非法访问 →panic 宕机用户态页表User Page Table定义每个进程私有的页表仅映射用户空间。根目录task_struct.mm.pgd生命周期进程创建 → 退出映射特性按需分配懒分配首次访问触发缺页才建立 PTE可换出不活跃页可换至 swap隔离不同进程相同 VA → 不同 PA特性权限U/S1用户态可访问缺页用户缺页 → 内核处理非法访问 →SIGSEGV 崩溃核心区别对比表维度内核页表用户态页表归属全局唯一所有进程共享进程私有每个进程一套映射空间内核空间高 128TB用户空间低 128TB权限位 U/S0仅内核态可访问1用户态可访问基址寄存器CR3内核态加载CR3进程切换时加载生命周期系统启动至关机进程创建至退出内存分配直接映射为主常驻内存按需懒分配可换出至 swap异常影响非法访问 → 系统 panic非法访问 → 进程 SIGSEGV变化频率低仅 vmalloc/fixmap 变动高malloc/free、栈增长频繁KPTIKPTI (Kernel Page-Table Isolation内核页表隔离也简称 PTI)是 Linux 内核x86 架构为抵御Meltdown熔断硬件漏洞而引入的核心安全机制通过双页表设计彻底分离用户态与内核态的地址空间阻止用户态通过旁路攻击窃取内核数据Meltdown 漏洞原理漏洞本质利用 Intel x86 CPU乱序执行与权限检查延迟的缺陷。攻击流程用户态代码非法访问内核地址本应触发权限错误。CPU 乱序执行时先读取数据并缓存后检查权限。权限异常抛出前攻击者通过时序分析侧信道攻击从缓存中推断出内核数据。传统页表缺陷进程页表CR3 指向同时包含用户空间与内核空间映射。内核虽设 U/S0 权限位但 Meltdown 可绕过此限制。KPTI 核心设计双页表隔离KPTI 为每个进程维护两套完全独立的页表并在态切换时切换 CR3 寄存器1. 内核页表 (Kernel Page Table)使用场景仅在内核态Ring 0运行时加载。内容完整映射内核空间 该进程的用户空间。作用保证内核正常访问所有内存如copy_to_user。基址mm-pgd传统 PGD。2. 用户页表 (User Page Table / Shadow Page Table)使用场景仅在用户态Ring 3运行时加载。内容完整用户空间映射。最小内核映射仅包含进入 / 退出内核必需的代码如系统调用入口、中断处理程序、IDT、CPU 入口 trampoline 代码。内核其余地址完全未映射PTE 为空。基址mm-pgd PAGE_SIZE与内核 PGD 连续存放于 8KB 对齐空间。工作流程态切换与页表切换1. 用户态 → 内核态系统调用 / 中断 / 异常CPU 捕获事件硬件切入内核态。执行汇编入口代码如entry_SYSCALL_64。切换 CR3从用户页表影子页表切换到内核完整页表。执行内核代码此时可访问全部地址空间。2. 内核态 → 用户态iret/sysret内核执行完毕准备返回用户态。切换 CR3从内核页表切换回用户影子页表。执行iret指令返回用户态。用户态运行时无法访问任何未映射的内核地址Meltdown 失效。关键优化PCID (Process Context ID)问题切换 CR3 会导致整个 TLB 被刷新TLB 清空后续访存需重新遍历页表性能暴跌。PCID 优化Intel 新 CPU 支持 PCIDASIDTLB 条目带进程 ID 标签。KPTI 为同一进程分配两个 PCID如 bit 11 区分0 内核1 用户。切换 CR3 时不刷新 TLBCPU 自动匹配对应 PCID 的缓存条目大幅降低开销。x86 vs ARM64 实现差异x86KPTI硬件仅一个 TTBRCR3需软件维护双页表 CR3 切换。用户态页表仅留最小内核跳板。ARM64内核页表隔离硬件原生支持两个 TTBR 寄存器TTBR0_EL1用户态页表EL0。TTBR1_EL1内核态页表EL1。修复 Meltdown 时改造 TTBR1使其在用户态时仅映射最小跳板代码无需切换 CR3。ARM64 内核页表隔离ARM64 的内核页表隔离KPTI/PTI在 Linux 中称为UNMAP_KERNEL_AT_EL0它利用 ARMv8 硬件双页表基址寄存器TTBR0/TTBR1的天然优势实现了比 x86 更高效、开销更低的 Meltdown 漏洞防护。硬件基础原生双页表设计ARM64AArch64从架构上就分离了用户与内核地址空间地址划分用户空间 (EL0)0x0000_0000_0000_0000 ~ 0x0000_ffff_ffff_ffff高位全 0内核空间 (EL1)0xffff_0000_0000_0000 ~ 0xffff_ffff_ffff_ffff高位全 1硬件寄存器TTBR0_EL1(Translation Table Base Register 0)用户态页表基址仅用于低地址用户空间。TTBR1_EL1(Translation Table Base Register 1)内核态页表基址仅用于高地址内核空间。MMU 行为CPU 自动根据虚拟地址第 63 位选择页表0 → TTBR01 → TTBR1Meltdown 漏洞在 ARM64尽管硬件隔离但受影响的核心主要是Cortex-A75及部分新架构存在推测执行漏洞。在EL0 (用户态)时CPU 仍可能通过TTBR1投机性访问完整的内核地址空间并留下缓存痕迹导致信息泄露。ARM64 KPTI 核心原理Trampoline (跳板) 页表ARM64 不采用 x86 那样的双 CR3 切换而是改造 TTBR1维护两套内核页表两套内核页表完整内核页表 (swapper_pg_dir)使用场景内核态 (EL1)运行时。内容映射全部内核空间代码、数据、直接映射区、vmalloc 等。权限AP00(仅 EL1 可访问)。Trampoline (跳板) 页表使用场景用户态 (EL0)运行时。内容仅映射最小内核入口代码异常向量表vectors、 trampoline 代码。作用提供从 EL0 进入 EL1 的 “独木桥”内核其余地址完全未映射。工作流程态切换时切换 TTBR1(1) 用户态 → 内核态 (异常 / 系统调用)事件发生CPU 硬件切入 EL1自动保存上下文。执行位于Trampoline 页表中的异常向量入口vectors。切换 TTBR1从Trampoline页表切换到完整内核页表。ldr x1, swapper_pg_dir // 加载完整内核页表物理地址 msr ttbr1_el1, x1 // 写入 TTBR1 isb // 同步指令流执行正常内核代码。(2) 内核态 → 用户态 (eret 返回)内核准备返回用户态。切换 TTBR1从完整内核页表切换回 Trampoline 页表。ldr x1, trampoline_pgd // 加载跳板页表物理地址 msr ttbr1_el1, x1 // 写入 TTBR1 dsb nshst // 确保页表写入完成 tlbi vmalle1 // 无效化内核 TLB isb执行eret返回 EL0 用户态。用户态下任何访问内核高地址的企图包括投机访问都会触发Translation Fault且无数据可泄露。关键要点本质利用 ARM64双 TTBR 架构在用户态时用极小的 Trampoline 页表替换完整的内核页表让用户态 “看不见” 内核数据。优势硬件原生支持、切换轻量、TLB 友好、开销极小是比 x86 更优雅的隔离方案。目的彻底阻断 Meltdown 利用 ** speculative execution TTBR1** 窃取内核数据的路径。内核页表与用户页表的关联1. 传统模式无 KPTI进程页表 用户页表 内核页表副本每个进程 PGD 中内核空间部分高 128TB完全相同均复制自swapper_pg_dir进程切换时只切换用户空间页表内核部分共享2. KPTI 隔离模式Meltdown 漏洞修复Linux 4.15为防止用户态侧信道读取内核数据引入双页表内核页表完整含全部内核 用户映射仅内核态使用用户页表用户态仅含用户空间 最小内核入口映射如系统调用 / 中断门、CPU entry areaLinux Kernel切换机制用户态 → 内核态syscall / 中断切换到完整内核页表内核态 → 用户态iret切换到用户隔离页表为什么很多人会误以为 “内核页表包含用户页表”因为早期 Linux无 KPTI、无隔离的设计是每个进程的 PGD 同时挂载了用户空间映射 内核空间映射看起来像进程页表CR3/TTBR0 ├─ 用户空间映射每个进程私有 └─ 内核空间映射所有进程共享于是产生错觉“内核页表包含用户页表”其实是反过来进程页表里 “顺带” 放了内核的映射条目但内核自己的页表swapper_pg_dir从来不包含任何用户页表。用 ARM64 最直观说明ARM64 硬件天生分开TTBR0_EL1只管用户虚拟地址低 64 位内容用户页表特点每个进程一套互不共享TTBR1_EL1只管内核虚拟地址高 64 位内容内核页表特点全局唯一所有进程共享关系是内核页表只映射内核空间完全不碰用户地址用户页表只映射用户空间完全不碰内核地址互相独立互不包含MMU 按地址高位自动选页表。传统模式和KPTI 模式区别没开 KPTI 时用户页表里包含内核地址空间映射开了 KPTI 后用户页表里几乎不包含内核映射只留一点点入口用户页表 用户空间映射 完整内核空间映射不管是 x86_64 还是 ARM64每个进程的页表CR3 / TTBR0低地址自己的用户空间私有高地址完整内核空间所有进程共享同一份映射内核为什么要这么设计进入内核时不用换页表系统调用、中断直接用当前页表就能访问内核数据切换进程只需要换用户部分内核部分不动所以用户态页表 → 包含完整内核地址空间映射只是用户态访问时会被权限位U/S、AP拦住不能真的读写。开启 KPTI / UNMAP_KERNEL_AT_EL0 之后用户页表里 只有用户空间 极少内核跳板代码x86_64 KPTI用户态用的那套页表内核大部分地址完全没映射只映射系统调用入口、中断入口、异常向量等一小段代码ARM64 UNMAP_KERNEL_AT_EL0用户态时 TTBR1 指向 trampoline 页表只有异常向量表那一点点内核代码其余内核地址没有映射完全看不见所以用户态页表 ≠ 包含完整内核映射只保留 “进内核” 必需的最小跳板。关键机制与代码1. 页表切换进程切换context_switch加载新进程mm-pgd到 CR3KPTI 切换pti_switch_user()/pti_switch_kernel()切换 CR32. 内核页表同步内核修改页表如 vmalloc先改swapper_pg_dir进程缺页时自动将内核页表项同步到进程页表3. 核心数据结构// 进程地址空间 struct mm_struct { pgd_t *pgd; // 页全局目录用户页表根 // ... }; // 内核主地址空间init_task struct mm_struct init_mm { .pgd swapper_pg_dir, // 内核页表根 // ... };总结内核页表全局、共享、高权限、稳定、常驻服务内核自身用户页表私有、隔离、用户态可访问、动态、可换出服务进程传统进程页表包含内核副本KPTI双页表完全隔离提升安全

更多文章