选择子里有一个请求特权级,描述符里有一个描述符特权级。前面把这两个字段的位置都点过了,但一直没讲它们到底怎么用。
这一篇把这件事补完。CPU 往段寄存器里装选择子,不是随手一写。它先比三个数——CPL、RPL、DPL——过了才装得进去。数据段和栈段的规则不一样。
方向先钉死,全文只说这一次:0 最特权,3 最不特权,数字越大权限越低。
保护模式下,往 CS、SS、DS、ES、FS、GS 里装选择子,处理器会先取出目标描述符,比三个数。通过了才改段寄存器;通不过就抛 #GP,段寄存器原封不动。
三个数各住在不同的地方:
CS 的低 2 位日常写内存不会再把这三个数拿出来比一遍。检查发生在装段那一刻。所以这一篇的主语是「装段」,不是「访问内存」。
手册把特权级画成四个同心圆。圆心 Level 0 是内核,最外圈 Level 3 是应用程序,中间 1 和 2 留给操作系统服务。

图 1:Vol. 3A §6.5 的 Figure 6-3「Protection Rings」(印刷页 6-7)。数字越大,越不特权。
手册原话把方向钉死:4 个特权级,编号从 0 到 3,数字越大特权越低。同一段还有一句括号:只打算用两级的系统,应当使用 0 和 3。
Windows 照做。Win7、Win10、Win11,x86 和 x64,日常执行层级都只留 0 和 3。1 和 2 在硬件上还在,操作系统没有给它们通电。所以后面所有例子只出现 0 和 3。
日常说的「零环」「三环」,就是这张图上的 Level 0 和 Level 3。
CPL 没有单独的寄存器。它就住在当前 CS 选择子的低 2 位。换代码段,CPL 才变。改 DS、ES,人还站在原来那一环。
课上特别强调过一句:「零环程序」「三环程序」这种说法不准确。
一个普通程序从三环拉起来,但很多功能最终要进内核才能做。进内核的那一刻断下来,CPU 正在执行零环代码,CPL 就是 0;返回用户态再断,CPL 又是 3。变的不是这条进程的身份证件,是 CS 被换成了另一段代码。
所以要问的不是「这是一个零环程序还是三环程序」,而是:这一刻,CPU 在执行哪一环的代码。
看选择子低两位就能读出来。不同系统、32 位和 64 位,具体数字可以不一样,只看最后两位:
| 现场 | 常见选择子 | 低 2 位 |
|---|---|---|
| 用户态代码 | CS = 0023 | 3 |
| 用户态栈 | SS = 002B | 3 |
| 内核代码 | CS = 0008 | 0 |
| 内核栈 | SS = 0010 | 0 |
CS 和 SS 的特权级必然相同。代码跑在零环,却拿着三环的栈,硬件不允许。零环有零环的栈,三环有三环的栈。
64 位 Windows 上,用户态常见 CS = 0033,内核常见 CS = 0010。数字换了,读法没换。
三个名字不是一个概念写了三遍。用三句问句分开:
| 名称 | 问句 | 住在哪 |
|---|---|---|
| CPL | 当前是谁在执行? | 当前 CS 低 2 位 |
| RPL | 这次访问以谁的名义提出? | 段选择子低 2 位 |
| DPL | 目标段允许到哪一层? | 描述符 Access 的 DPL |
RPL 和 DPL 的住址,手册各有一张图。

图 2:Vol. 3A Figure 3-6「Segment Selector」(印刷页 3-7)。最右边两位就是 RPL。
同一条描述符可以有四张不同的选择子。0018 和 001B 指向 GDT 里同一项,差别只在 RPL 是 0 还是 3。装段时,硬件认的是这一次手里拿的那张。

图 3:Vol. 3A Figure 3-8「Segment Descriptor」(印刷页 3-10)。DPL 在 Access 字节里 P 下面那两位。
DPL 焊在描述符上,建 GDT 的时候就写死了。内核段写成 0,用户段写成 3。它不是这次访问临时算出来的。
CPL 其实也是从选择子低两位读出来的——当前 CS 的 RPL,在正常执行时就是 CPL。但必须先定「人现在站在哪」,再拿这一次的 RPL 和目标 DPL 去比。
数据段写在手册 §6.6。装 DS、ES、FS、GS 的条件是:
max(CPL, RPL) ≤ DPL
max 取数字更大的那个,也就是更不特权的那个。谁更不特权,就按谁算。
Windows 只用 0 和 3,左边两个数的组合其实很少。
两边都是 0,max 就是 0,任何 DPL 都过——人在零环,拿的也是零环的选择子,内核数据、用户数据这一关都放行。
左边有一个是 3,max 就是 3。3 小于等于 0?不成立。DPL=0 的内核段装不进去,只能去装 DPL=3 的用户数据段。
三条例子对着数字看:
| 情形 | CPL | RPL | DPL | 结果 |
|---|---|---|---|---|
| 内核读用户数据 | 0 | 0 | 3 | 过。0 ≤ 3 |
| 三环装自己的数据段 | 3 | 3 | 3 | 过。3 ≤ 3 |
| 三环装内核段 | 3 | 0 | 0 | 不过。3 ≤ 0 |
第三行把 RPL 改成 3,还是过不了。挡下来的是 CPL 已经大于 DPL。
栈段不要套这条 max。手册 §6.7 说,装 SS 的时候,CPL、RPL、DPL 三个数必须相同。差一个,直接 #GP。
原因在第三节已经见过:CS 和 SS 必须同级。代码跑在零环,不能去用三环的栈;反过来也不行。栈是压返回地址的地方,用错了环,等于把现场写到别人能改的内存里。
所以不是「够特权就行」。零环去装用户栈,特权够高,按数据段公式能过,按栈规则照样失败。
代码段的检查另有说法,而且会碰到门。那一块还没讲,本篇先停在数据和栈。
第一句:Windows 只使用 0 和 3。
第二句:CPL 在当前 CS,RPL 在选择子,DPL 在描述符。先看这一刻 CS 的低两位,再看要装的那张选择子,最后看目标段自己的门槛。
第三句:数据看 max,栈段要同级。检查只发生在装段寄存器的时候,不是每条访存指令都做一次。
上面结论对照 Intel SDM 第 091 版核过,主线落在 Vol. 3A 的 §6.5、§6.6、§6.7。文中三张图均从该版手册截取,未做改动。
觉得这种把一条规则拆到手册原图上的写法有用,就点个「在看」。代码段怎么换、人要进圆心的合法入口在哪里,下一篇再拆。