在x86_64的长模式(Long Mode)下,段机制发生了根本性的简化,段基址和段限长检查被弱化,但权限检查(即特权级检查)仍然至关重要。长模式下的段权限检查,实际上只围绕一个核心规则:数据段的访问权限检查被大幅简化,但代码段和控制转移的权限检查依然严格。
一、长模式下的关键变化(与保护模式对比)
| 特性 | 保护模式(32位) | 长模式(64位) |
|---|---|---|
| 段基址 | 可任意指定 | CS/SS/DS/ES 基址强制为0(FS/GS可通过MSR独立设置) |
| 段限长 | 有效(基于Limit字段) | CS/SS/DS/ES 段限长被忽略(视为全64位地址空间) |
| 分段检查 | 每个内存访问都检查基址+限长 | 仅对FS/GS进行基址/限长检查,其他段跳过 |
| 权限检查(DPL/RPL) | 严格检查 | 仍然严格检查(特别是代码段和控制转移) |
| 段类型检查 | 完整检查(代码/数据/系统段) | 简化为代码段(CS)和数据段(DS/SS/ES)两类 |
| CALL/JMP门 | 支持任务门、调用门等 | 不再支持任务门/调用门(使用中断门/陷阱门替代) |
二、长模式下的段权限检查核心机制
1. 当前特权级(CPL)的确定
CPU通过CS段选择子的RPL(请求特权级)字段来确定当前特权级(CPL),而不是通过段描述符中的DPL。具体地,在长模式下:
CS.RPL = 0:内核态(ring 0)
CS.RPL = 3:用户态(ring 3)
注意:长模式只支持ring 0和ring 3,ring 1和ring 2在长模式下没有实际用途(但段描述符中的DPL仍然允许设置为1或2,只是操作系统通常不使用)。
2. 数据段访问权限检查(DS/SS/ES)
长模式下,数据段的权限检查简化为仅检查CPL与段描述符DPL的关系:
| 条件 | 结果 |
|---|---|
CPL <= DPL(数值上:0 ≤ 3?不对,数值越小特权越高) | 允许访问 |
CPL > DPL | 触发#GP(通用保护异常) |
具体规则:
CPL=0(内核)可访问任何DPL的数据段(DPL=0/1/2/3)
CPL=3(用户)只能访问DPL=3的数据段(不能访问DPL=0/1/2)
示例:
; 用户态代码(CS.RPL = 3) mov rax, [0x12345678] ; 数据段默认为DS(用户态DS指向DPL=3的段)→ 允许 mov rax, [fs:0x10] ; FS基址独立,权限检查基于GS的段描述符(通常也是DPL=3)→ 允许
如果用户态试图加载一个DPL=0的内核数据段到DS,会触发#GP。
3. 代码段访问与控制转移检查
这是长模式下权限检查最复杂的部分,涉及代码段切换和控制转移指令(JMP、CALL、RET、IRET等)。
a. 直接转移(JMP/CALL到同一代码段)
在同一代码段内跳转:不涉及CPL变化,只检查目标地址是否在当前段限长内(但段限长被忽略,所以总是允许)。
到不同代码段的跳转(通过
JMP/CALL远跳转):在长模式下,直接远跳转(使用JMP FAR)被禁止,只能通过中断门/陷阱门或系统调用(SYSCALL/SYSRET)实现跨段转移。
b. 通过中断门/陷阱门的控制转移(异常和中断)
这是长模式中最常见的跨段转移方式。CPU通过IDT(中断描述符表)中的门描述符来转移控制:
权限检查条件:
当前特权级(CPL)必须等于中断门的DPL(或者数值上小于DPL,即特权级更高?不,对于中断门,要求
CPL >= DPL?这里需要澄清):用户态(CPL=3)可以通过
int 0x80触发一个DPL=3的中断门(例如Linux的系统调用)。内核态(CPL=0)触发任何中断(通常内核中断门的DPL=0)。
CPL <= 目标代码段的DPL?不,对于中断门,目标代码段的DPL通常是0(内核代码段),但中断门本身有DPL,权限检查的是:CPL必须 <= 中断门的DPL(数值上,0 <= 3,内核可触发任何中断;3 <= 3,用户可触发DPL=3的中断)。目标段描述符的DPL(代码段的DPL)决定了目标代码段运行的特权级(通常是0)。
示例(Linux系统调用int 0x80):
用户态(CPL=3)执行
int 0x80。IDT中的
0x80中断门的DPL=3(允许用户态触发)。目标代码段(
__KERNEL_CS)的DPL=0(内核态)。权限检查:
CPL(3) <= 中断门DPL(3)→ 允许,转移到内核代码段(CPL变为0)。
c. 系统调用(SYSCALL/SYSRET)
长模式下的系统调用不经过中断门,直接使用SYSCALL指令,它绕过了一般的段权限检查,但依然通过MSR(IA32_STAR、IA32_LSTAR)指定目标代码段和栈,并在执行时自动切换CPL(从3到0),返回时通过SYSRET切换回用户态(从0到3)。
d. 返回指令(RET/IRET)
IRET(从中断返回):会检查返回目标代码段的CPL是否与栈上的CS选择子的RPL一致,如果不一致(例如内核返回到用户态),会触发#GP。RET(远返回):在长模式下,RETF(远返回)是不允许的(会被忽略或触发#GP)。
三、长模式下的段权限检查流程(状态机)
┌─────────────────┐ │ 当前 CPL │ = CS.RPL (从段选择子读取) │ (0 或 3) │ └────────┬────────┘ │ ▼ ┌───────────────────┐ │ 数据访问(DS/SS/ES) │ │ 检查:CPL <= 目标段描述符的 DPL │ └────────┬──────────┘ │ ▼ ┌───────────────────┐ │ 代码转移(JMP/CALL/IRET) │ │ 1. 检查中断门/陷阱门的 DPL │ │ 2. 检查目标代码段的 DPL │ │ 3. 检查栈段(SS)的 DPL │ └────────┬──────────┘ │ ▼ ┌────────────────────┐ │ 返回(IRET) │ │ 检查返回目标 CS 的 RPL 与当前 CPL │ └────────────────────┘
四、段寄存器在长模式下的实际角色
| 段寄存器 | 基址 | 段限长 | 权限检查角色 |
|---|---|---|---|
| CS | 0(固定) | 忽略 | 确定CPL(通过RPL字段);代码执行权限 |
| SS | 0(固定) | 忽略 | 特权级切换时确定是否切换栈(通过IRET) |
| DS | 0(固定) | 忽略 | 数据访问权限检查(CPL <= DPL) |
| ES | 0(固定) | 忽略 | 同DS |
| FS / GS | 独立MSR设置 | 可设置 | 仍进行完整的基址/限长检查(用于线程局部存储、CPU核心数据) |
五、实际代码示例
// 尝试在内核态加载用户态数据段(x86_64) static inline void load_user_ds(void) { asm volatile ( "mov %0, %%ds\n" : : "r" (__USER_DS) // 用户态数据段选择子(DPL=3) : "memory" ); } // 如果在内核(CPL=0)中执行此函数,会触发 #GP // 因为 CPL(0) > 用户态DS的DPL(3)?不,CPL=0 <= DPL=3,应该允许! // 实际上,内核可以加载用户态DS(因为0 <= 3),但访问内存时,DS的基址为0,且权限检查基于CPL与DS.DPL的关系。 // 但出于安全考虑,Linux内核通常不这样做。六、总结:长模式下的段权限检查规则
| 操作 | 检查内容 | 失败结果 |
|---|---|---|
| 加载数据段(DS/SS/ES) | CPL <= 段描述符.DPL且段类型 == 数据段 | #GP |
| 加载代码段(CS) | 仅通过中断门/陷阱门或SYSCALL间接加载,不能直接LOAD | #GP |
| 中断/异常处理 | CPL <= 中断门.DPL且目标代码段.DPL == 0(通常) | #GP/#DF |
IRET返回 | 栈上CS.RPL == 目标CPL且CPL <= 目标栈段.DPL | #GP |
SYSCALL进入 | 无检查(硬件直接切换) | #GP(如果MSR配置错误) |
SYSRET返回 | 要求目标CS.RPL == 3 且 目标栈段.DPL == 3 | #GP |
七、常见误解澄清
“长模式下段寄存器被弃用了”→ 不准确。
CS和SS仍然用于特权级管理,FS和GS仍然用于TLS/CPU核心数据。“长模式下段限长检查被完全忽略”→ 仅对
CS、DS、SS、ES而言。FS和GS仍然有基址和限长(通过MSR设置)。“长模式下CPL只能从CS选择子读取”→ 正确。段描述符的DPL只用于数据段权限检查,不用于确定CPL。