内核段管理 —— x86-64 下的代码段/数据段/段权限
核心问题:用户态代码段
CS=0x33为什么无法访问内核数据段DS=0x18?syscall凭什么能瞬间从 ring 3 跳到 ring 0?内核到底怎么管理这些段选择子和描述符?本篇为导读:先给核心问题的答案,再速览三模式演进、核心机制(选择子/GDT/权限/syscall)、三层保护、寻址流水线,最后给出五个独立子篇的导航——完整机制拆解在各子篇中展开。
相关:本文是 syscall-details 中段管理部分的独立深化版本——如果你想先了解"系统调用整体过程",可以先看那篇。涉及大量段寄存器和 MSR 细节,不熟悉可先看 x86-64-registers;上下文切换时也涉及段寄存器保存/恢复,见 context-switch;中断进入内核走 IDT 门(另一条路),见 interrupts。
零、核心问题速览:为什么 0x33 碰不了 0x18
一句话答案:CPU 在"加载段寄存器"这个动作上执行权限检查 max(CPL, RPL) <= DPL,而内核段描述符的 DPL=0、用户态 CPL=3,怎么都过不了。
用户态尝试加载内核数据段:
mov ax, 0x18 ; 内核数据段选择子, DPL=0
mov ds, ax ; 检查: max(CPL=3, RPL=0) = 3 > DPL=0 → #GP(0)!
; 内核将 #GP 转为 SIGSEGV 递交给进程 → 进程崩溃但这里有个关键追问:用户代码不加载内核段,直接读内核地址(如 mov eax, [0xffff8800...])行不行? 答案是不行——因为还有第二层:页表 U/S 位(CPL=3 访问 U/S=0 的页 → #PF),以及第三层 KPTI(用户态页表里根本没有内核映射)。三段保护层层递进,详见GDT 与特权级切换篇。
一、三模式演进速览
x86 段机制经历三次重构:实模式 → 32位保护模式 → 64位长模式,脉络是"地址翻译逐渐退场、权限管理走到台前":
| 维度 | 实模式(16位) | 32位保护模式 | 64位长模式 |
|---|---|---|---|
| 段寄存器存什么 | 直接存段基址(16位) | 16位段选择子(索引+TI+RPL) | 同32位,选择子 |
| 段属性存哪里 | 无——寄存器即属性 | GDT/LDT 中 8 字节描述符 | GDT 描述符(基址/界限忽略) |
| 地址翻译 | 段×16+偏移 → 物理地址 | 基址+偏移→线性地址 | 偏移量=线性地址(扁平) |
| 权限检查 | 无 | CPL/RPL ≤ DPL | CPL/RPL ≤ DPL 严格执行 |
| 分页 | 无 | 两级(PDT+PT) | 四级(PML4+PDPT+PDT+PT) |
| 最大寻址 | 1MB | 4GB | 256TB(48位虚拟地址) |
一句话:x86-64 废掉了分段的地址翻译功能(基址强制 0、界限强制全覆盖),但完整保留了权限管理功能(DPL/RPL 检查)——"x86-64 去掉了段"的说法不准确。完整演进(含为什么这样设计的三个理由)见演进与段选择子篇。
二、核心机制速览:选择子 → GDT → 权限 → syscall
- 段选择子(16位令牌):高 13 位是 GDT/LDT 索引、TI 位选表、低 2 位 RPL。Linux x86-64 里用户态
CS=0x33(GDT[6] 索引 6×8=0x30 + RPL=3)、SS=0x2B(GDT[5] 0x28+3)、内核态CS=0x10(GDT[2])、DS/SS=0x18(GDT[3])——四个 ABI 常量永不变。 - GDT(配置中心):每个 CPU 一份,存 8 字节段描述符;权限全在 Access Byte 的 DPL 字段;内核代码段和用户代码段描述符只差一个 DPL 位(0 vs 3)。
- 权限检查公式:
max(CPL, RPL) <= DPL才通过,否则 #GP(0);RPL 的意义是防止低特权代码借用高特权选择子。 - syscall/sysret 是"免检"特权切换:硬件按 MSR_STAR 预设值直接写 CS/SS(ring 0 才能写该寄存器),
syscall时 CPL 3→0、sysret时 0→3;但硬件只切 CS/SS,内核入口还需手动swapgs、换 RSP、加载 DS/ES、KPTI 下切 CR3。
细节(段选择子结构、0x10/0x18/0x33/0x2B 的计算、GDT 逐位拆解、Access Byte、CPL/RPL/DPL 三方博弈、per-CPU GDT 与 TLS 管理、MSR_STAR 布局、syscall/sysret 完整汇编)见 演进与段选择子、GDT、权限检查与特权级切换(由原「GDT 与权限检查」「特权级与三层保护」两篇合并)两篇。
三、三层保护速览
| 保护层 | 机制 | 保护范围 | 可绕过吗 |
|---|---|---|---|
| 段 DPL | 加载段寄存器时检查 CPL vs DPL | 阻止加载内核段选择子 | Meltdown 推测执行可绕过 |
| 页表 U/S 位 | 每次内存访问时检查 CPL vs PTE.U/S | 阻止访问内核物理页 | Meltdown 推测执行可绕过 |
| KPTI 页表隔离 | 用户态页表中根本没有内核映射 | 物理上阻断所有内核地址 | 不可绕过 |
三层关系:段 DPL 拦"动作"(加载段选择子)、页表 U/S 拦"访问"(读内核物理页)、KPTI 拦"看见"(推测执行都拿不到)。用户态碰不到内核内存靠的就是这三层叠加。KPTI 的代价是每次 syscall/中断进出要切 CR3(PCID 优化),详见GDT 与特权级切换篇与 tlb。
四、寻址流水线速览
一条指令从取指到数据落地要经过:取指 → 解码 → AGU 算有效地址 → 段翻译(长模式透传)→ 规范地址检查(bit 47 符号扩展,不合法 #GP)→ TLB 查找(~1 拍)→ miss 则四级 page walk(CR3→PML4→PDPT→PD→PT,逐级查 P/R/W/U/S,大页可提前终止)→ 缓存层次(L1d 4-5 拍 / L2 12 拍 / L3 40-50 拍 / DRAM 100-300 拍)→ 数据写回寄存器。
一条
MOV EAX, [0x7FFF12345678]:乐观 ~12-15 拍、全 miss ~500+ 拍,差 40 倍——这就是"缓存/TLB 友好"不是口号的原因。完整走读(含 PTE bit 逐位检查、异常路径表、Page Fault 三种命运、六种寻址模式对比、性能透视)见分页与寻址实例篇;TLB 深度展开见 tlb。
五、观测速览
gdb 的 info registers 能直接看到理论预测的全部值:CS=0x33、SS=0x2b、DS/ES/FS/GS=0x0(长模式不检查基址/界限)、fs_base=0x7ffff7dd4740(glibc 设的 TLS 基址);rdmsr 0xC0000081 读 MSR_STAR;写一段 mov ds, 0x18 内联汇编必然 #GP → SIGSEGV 现场验证权限公式。实操步骤见观测调试篇。
六、与仓库其他文档的关系
- syscall-details — 系统调用深入细节,本文是其中段管理部分的独立深化
- syscall — 系统调用基础过程与开销分析
- interrupts — 中断处理,另一种进入内核态的方式(通过 IDT 门描述符,不同于 syscall)
- context-switch — 上下文切换时也涉及段寄存器的保存/恢复
- memory-layout — 用户态/内核态地址空间布局,段权限保护了这些地址的隔离
- tlb — TLB 与页表 U/S 位的关系,KPTI 通过 PCID 优化页表切换开销
- cache-organization — 缓存层次(寻址实例中的 L1/L2/L3/DRAM)完整展开版
- x86-64-registers — CR3/EFER/段寄存器详细定义
七、一句话总结
x86-64 的段管理废掉了地址翻译(基址和界限强制平坦),但完整保留了权限管理。GDT 是段管理的"配置中心",每个段描述符 8 字节,其中 DPL 字段决定该段的最小访问特权级。CPU 在加载段寄存器时执行
max(CPL, RPL) <= DPL检查,用户态 CPL=3 永远无法加载 DPL=0 的内核段。syscall指令绕过正常段加载检查,通过 MSR_STAR 直接写入预设的内核 CS/SS(0x10/0x18),实现 CPL 从 3 到 0 的合法跳转。页表 U/S 位和 KPTI 页表隔离构成第二、第三层保护。这三层共同保证:即使用户态知道内核地址,也无法读写内核内存——段选择子加载不了、页表 U/S 位拦着、KPTI 下页表里根本没有映射。完整的地址翻译则走"段透传 → 规范检查 → TLB → 四级页表 → 缓存"流水线,cache/TLB 命不命中性能差 40 倍。
拆分导航
本篇为拆分后的导读页,四个主题各自独立成文:
| 文档 | 内容 |
|---|---|
| 演进与段选择子 | 三模式演进(实模式→32位保护→64位长模式)+ 破除误解 + 段选择子结构(0x33/0x2B/0x10/0x18 的计算) |
| GDT、权限检查与特权级切换 | GDT 结构 + 8 字节描述符逐位拆解(Access Byte)+ max(CPL,RPL)<=DPL + CPL/RPL/DPL 三方博弈 + syscall/sysret 免检切换(MSR_STAR)+ 段 DPL/页表 U/S/KPTI 三层保护 + per-CPU GDT/TLS 管理 |
| 观测调试 | gdb 实测段寄存器快照对照 + GDT/MSR 查看 + 触发 #GP 的权限验证实验 |
| 分页与寻址实例 | 三类地址 + 32位两级/64位四级分页 + 一条 MOV 指令完整寻址走读(TLB/page walk/缓存/性能透视) |