1. 特权级从哪来:一个关于“保护”的古老故事
展开x86处理器的 Ring 0~Ring 3 权限模型之前,先回到上世纪80年代初的原点。x86架构第一次引入特权级概念,是在80286时代,这个设计初衷特别朴素:一台电脑可能同时跑操作系统和用户程序,操作系统凭什么不让自己被用户程序搞死?放在今天,你很难想象一个普通进程崩溃会把整个系统带崩盘,但这些保护机制不是凭空长出来的,它们的根就在特权级这套规则里。
x86的硬件设计者当时面对的是一个很直白的矛盾:程序需要访问各种硬件资源,但你不能让每个程序都有同等访问权限。如果一个普通应用能随便改中断描述符表、随便向IO端口写数据,那这台机器的稳定性就是零。于是硬件层面引入了一套“圈层”体系——Ring,也叫特权级或保护环。CPU把运行中的代码按照特权级分成四个级别,Ring 0 特权最高,Ring 3 特权最低。大多数现代操作系统只用到两个极值:操作系统内核待在 Ring 0,应用程序待在 Ring 3。Ring 1 和 Ring 2 在很长一段时间里几乎没人用,后面细说。
设计逻辑其实比大多数人想的简单:低特权级的代码不能随便执行特权指令,也不能直接访问高特权级的数据结构,凡是越权的行为,CPU 直接抛异常、中断,交给 Ring 0 去裁决。这个思路放到今天依然是操作系统的地基,没有这套硬件配合,软件层面做得再花哨也兜不住底。
这套模型跟日常生活的类比很直接:Ring 0 像银行金库的管理员,钥匙、保险柜、账本全部掌在手里;Ring 3 像普通客户,只能通过柜台窗口办业务,你没法自己绕过窗户冲到金库里翻账本。客户要是非冲进去,银行保安(硬件保护机制)会拦住,轻则警告,重则直接带走。
但对从业者来说,知道“有几个环”远远不够。你迟早会遇到这么几个问题:CPL、DPL、RPL 这些缩写到底是啥?用户态到内核态切换时硬件到底发生了什么?VMware、Docker 这些东西跟特权级有什么关系?为什么当年有些 VMM 的设计要伪造特权级?我在这篇里一层层拆给各位看。不需要你提前懂很多底层知识,但如果你写过一点汇编、看过一点内核代码,理解速度会快很多。
2. 四个 Ring 的真实面貌:谁住在哪一层,为什么 Ring 1/2 坐了冷板凳
2.1 特权级的硬件定义:CS 段寄存器里的两位
Ring 这个概念最终落在 CPU 内部一个很具体的地方:当前代码段寄存器 CS 的 RPL(Requested Privilege Level)字段里。CS 寄存器里有一个 CPL(Current Privilege Level),占两位二进制位,取值 0、1、2、3,正好对应 Ring 0 到 Ring 3。CPU 每取一条指令,就是带着 CPL 这个身份标记去执行。
这个设计意味着什么?权限不是某个页面或者某段内存自带的标签,而是跟随“当前正在执行的代码”走的。你的程序在 Ring 3 跑得好好的,一旦通过某种机制切换到 Ring 0 的内核代码,CPU 的 CPL 就变成 0。这不能靠软件自己拍脑袋改,硬件提供专门的切换通道,后面讲 syscall 和 int 指令时细聊。
四个级别之间的核心差别在于“能做什么”:
| 特权级 | 允许执行的指令/资源 | 典型使用者 |
|---|---|---|
| Ring 0 | 所有特权指令,包括 LGDT、LIDT、LTR、MOV CRx、HLT、IN/OUT、CLI/STI 等,可直接操作页表、中断、IO端口 | 操作系统内核、Hypervisor、固件的一部分 |
| Ring 1 | 只能执行部分特权指令,无法访问某些关键控制寄存器 | OS 服务层(历史上一些 OS 用过),如驱动 |
| Ring 2 | 特权指令限制更多,常用于某些设备驱动 | 极少使用 |
| Ring 3 | 几乎所有特权指令都被禁止,只能通过门机制或系统调用接口请求操作系统代劳 | 用户态应用 |
值得注意的细节:早期 x86 架构回忆录里,你会看到“分为 4 级,但实际大家只用 0 和 3”的说法。这不是懒,而是有合理性。操作系统的核心原则是“简单即安全”,多一层特权级意味着多一层跨级调用、多一种攻击面。Ring 1 和 Ring 2 为了兼容某些 OS 而保留,但在现代主流系统里基本是摆设。某些嵌入式场景或者研究型 OS 会考虑用中间两级,比如把驱动放在 Ring 1,把调度器放 Ring 0,但这种设计并没有成为主流。
2.2 为什么 Ring 1/2 被主流系统“战略性放弃”
如果你今天翻开经典操作系统教材,里面的图大概率画了一圈一圈的三个同心圆,标注待权级 0 到 3。但你翻 Linux、Windows 的实现,它们只用了 0 和 3。原因主要有两条:
第一,分级越多,跨级切换的成本越高、越复杂。每一次跨级切换,CPU 要换栈、保存状态、检查描述符权限,如果存在三级、四级跳转,光上下文切换的代码就够你喝一壶。OS 实现者的核心权衡是:我究竟要多少级保护?Ring 0 和 Ring 3 的两层模型足够覆盖绝大多数需求。
第二,很多硬件层面已经不再区分“中间级别”。比如 IO 权限检查依赖 IOPL 和 TSS 里的 IO 位图,这套机制对 Ring 1/2 也一样生效,但实际主流 OS 不在中间 Ring 安排代码,所以连带着相关的硬件校验路径都变成了闲置设备。
有个容易混淆的点:AMD64 和 Intel 64 在长模式下,部分特权级相关语义有变化。比如 64 位模式下,某些以前需要 Ring 0 才能执行的指令,现在只要不在 Ring 3 就行,包括 LGDT 等仍限制在 CPL=0,但有些“原先 Ring 0 才能读的系统寄存器”放宽了权限。做底层开发时,不能只看 32 位的教科书,得对着对应架构手册核一遍。
2.3 一个冷知识:分段机制反而成了特权级的第一道防线
很多人一提到保护机制就想到页表,实际上 x86 的保护是“分段先行,分页后补”。在 32 位保护模式下,你写的每一个地址,CPU 首先会拿它去做段式翻译,段式这个步骤里就含了特权级检查。一个段描述符里有 DPL(Descriptor Privilege Level),它表示“想要访问我这个段,你的 CPL 至少得有几级权限”。
举个例子:内核数据段描述符的 DPL 通常设为 0,那 Ring 3 的进程直接访问这个段就会被 CPU 拦下。用户态代码段描述符的 DPL 通常设为 3,所以 Ring 0 的内核也能使用用户段里的数据——高特权级能向下兼容低特权级,反向不行。
这种“单向访问”规则是整个特权级系统的灵魂。你代码里写一个指针指到内核地址,然后想直接读出内容?CPU 在做地址翻译之前先用段检查把你挡住了。如果段检查也过了,还有页表里的 U/S 位等着。两层检查的兜底设计,给了操作系统很强的抗误操作能力。
3. 权限检查到底怎么发生:CPL、DPL、RPL 的三角关系
3.1 三个缩写容易弄混,一次说清
底层开发的人嘴里天天挂着 CPL、DPL、RPL、IOPL,但它们各有各的管辖范围,不能混。
- CPL(Current Privilege Level):CPU 当前执行的代码所在特权级,存在 CS 寄存器低两位。每次取指、访存、执行 IO,CPU 都拿它做权限基准。我们平时说“我现在在 Ring 0 还是在 Ring 3”,翻译过来就是在问 CPL 是多少。
- DPL(Descriptor Privilege Level):存在于段描述符、门描述符、页表项里,表示“这个资源要求什么特权级才能访问”。比如中断门描述符的 DPL 设定的就是用户态能否通过 int 指令触发这个中断。
- RPL(Requested Privilege Level):藏在段选择子的低两位里,表示“发起这次访问的程序,声称自己有什么特权级”。它比 CPL 多了一层“自觉降级”的检查。为什么需要它?是为了防止高特权级代码替低特权级代码去访问更高级别的数据时越权,典型例子是
lsl、arpl这类指令。
权限校验时有个简单判断:CPU 会拿 CPL 和 RPL 中更低的那个(数值更大)去跟 DPL 比。也就是实际有效特权级 = max(CPL数值, RPL数值)。如果 CPL=0、RPL=3,那实际就是按 Ring 3 去访问目标段;只有有效特权级的数值 ≤ DPL 时,访问才被允许。
3.2 页表里的 U/S 位:特权级落地的最后一棒
段级检查过了之后,现代 OS 真正的主战场在分页。页表项/页目录项里有个 U/S 位(User/Supervisor)。U/S=0 表示只有 CPL=0,1,2(也就是“超级用户”)能访问;U/S=1 表示 CPL 任意级别都能访问。
你看,页表这一层没有用“四位特权级”去严格对齐,而是做了个二分法——Supervisor(CPL 0~2)和 User(CPL 3)。这也是为什么许多底层逆向的同事会发现:一个进程即使找到了某个内核地址,也没法直接 dump 内容,因为那个物理页被标记为 Supervisor-only,硬件直接就 page fault 了。
这层检查每个内存访问都会执行,无论你是取指令、读数据还是写数据。CPU 在这里不信任你程序的意图,只看它实际的 CPL。这种“物理层面的强制隔离”,构成了隔离用户态与内核态的最大底气。
3.3 还要留意一个旁路:IO 权限的 IOPL 和 IO 位图
内存保护之外,特权级还管着 IO 访问。IN、OUT、INS、OUTS这些指令从前是妥妥的特权指令,CPL 不够会被拦。但后来为了性能给了一条路子:如果 CPL ≤ IOPL(IO 特权级,存在 EFLAGS 寄存器里),可以直接执行 IO 指令。
这有点反常——IO 权限没有严格绑定到 Ring 0,而是看你当前的特权级有没有达到 IOPL 的标准。IOPL 默认是 0,但操作系统可以改。
还有一种更细的玩法:TSS(任务状态段)里有一个 IO 允许位图,可以针对特定端口放开权限。比如某个设备驱动程序跑在 Ring 3,但需要访问固定地址的端口,你可以把那个端口对应的位图位置成 0,让 Ring 3 程序绕过特权级检查直接访问 IO。写驱动的人可能遇到,做虚拟机的同事也经常会跟 IO 位图打交道——毕竟模拟各种设备时,IO 端口拦截是逃不开的。
4. 用户态和内核态之间怎么切换:系统调用、中断、门机制一锅端
4.1 切换的本质:换栈、换 CS、换权限检查的基准
CPL 不是软件随便改的。你总不能让一个 Ring 3 进程直接把 CS 的低两位改成 0,然后回到用户态继续执行——CPU 在有特权级变化的跨段跳转时有一套完整的“门检查机制”:
- 普通
jmp far或call far直接跳到一个更高特权级的代码段,会触发 #GP 异常。 - 唯一被允许的通路是:通过调用门、中断门、陷阱门,或者通过
syscall/sysenter指令,由硬件协助完成特权级提升。
在实际系统中,最常见的切换路径就是“用户态应用触发系统调用”。以一个read()为例,动作拆解如下:
- 用户态执行
syscall或int 0x80,CPU 捕捉到这次调用。 - CPU 从 MSR 寄存器或 IDT(中断描述符表)里找到目标内核入口地址。
- 检查目标门的 DPL 是否允许当前 CPL 进入。
- 把当前 SS、RSP、CS、RIP、EFLAGS 压入内核栈(TSS 里指定的栈)。
- CPL 变成 0,跳到内核入口。
- 内核处理完系统调用,通过
sysret/iret返回用户态,CPL 降回 3。
看这个过程,你会明白一个关键点:切换的特权级时,伴随的不只是那个“级别数值”的变化,还有执行栈的切换。Ring 0 有自己的内核栈,Ring 3 有自己的用户栈。两个栈之间靠 TSS 里的 RSP0/RSP1/RSP2 字段挂钩。
很多人刚开始接触时有个误解:“我只要用int 3或者int 0x80就能进内核”。这里要澄清一下,中断描述符表里每一项(门描述符)也有 DPL,如果 DPL 设置不对,Ring 3 发起的int指令直接触发通用保护异常。比如调试器常用的int3,如果 DPL 是 0,那普通程序就不能自己触发,防止恶意程序扰乱调试机制。
4.2 从 int 0x80 到 syscall/sysenter:传统与新贵的对决
Linux 在 x86 上早期用int 0x80做系统调用入口,如今主流 x64 平台走的是syscall指令(AMD 那边习惯叫 SYSCALL/SYSRET,Intel 也有)。两者差异很大:
int 0x80走 IDT,通过中断门描述符跨级,需要查内存中的描述符表,路径较长,而且容易受到 Spectre 类预测侧信道影响。syscall走的是 MSR(型号相关寄存器),直接用STAR/LSTAR等 MSR 里预先配置的入口地址,不需要查内存里那张描述符表,路径短、快,而且 CPU 会自动屏蔽某些中断,完成关键状态保存。
对普通应用开发者,这个过程透明。对底层开发,意义就大了:做系统调用监控、做热补丁、做 Fuzzing 的时候,你到底该 hook 哪里?int 0x80可以在 IDT 层面拦截,syscall则要在 MSR 层面或者内核入口处拦截,捕获方式完全不同。
4.3 调用门:被遗忘的正式通道
你翻老教材,里面一定会讲调用门(Call Gate)。调用门允许一个 Ring 3 的程序,把代码段选择子指向一个描述符,这个描述符标记为“门”,门里面有目标段选择子、偏移,还有 DPL。CPU 检查完 DPL 后,会帮你做特权级提升。
现代系统里,调用门几乎绝迹——因为syscall/sysenter以及中断门/陷阱门已经能满足一切需要,调用门的额外机制和支持软件的复杂度不值得。但偶尔逆向老驱动或老 OS 时,还能撞见这种结构。研究它还有一个价值:理解“门”这个概念,对理解 Xen、VMware 的 hypercall 机制、VMCALL 指令有很大帮助。门的作用就是“留一条受控的通道”,当代虚拟化技术其实到处都在用这个思想,只是换了个叫法。
5. 现代系统怎么“玩”特权级:虚拟化、微内核与容器隔离的暗厢操作
5.1 虚拟化最核心的一个逻辑:谁拥有真正的 Ring 0
虚拟机监控器(Hypervisor)出现之后,特权级的使用出现了一个尖锐矛盾:你希望客户机操作系统以为自己住进了 Ring 0,但底层真正的 Ring 0 得归 Hypervisor 所有。如果客户机的内核执行了特权指令,又不能真的操作物理 CPU 的完整特权域,那怎么办?
这里有历史演进的三个阶段:
- 早期纯软件模拟时代,CPU 会把敏感指令产生的特权级异常抛给 VMM,VMM 模拟执行后再把控制权交回客户机。这就是经典的“陷入-模拟”(trap-and-emulate),缺陷是性能差,敏感指令多的时候慢到发指。
- 后来 Intel/AMD 在硬件层面加了 VT-x/AMD-V,提供了一种全新的运行模式:VMX root 和 VMX non-root。客户机内核不再需要真的位于 Ring 0,它只是在 VMX non-root 模式下运行,特权级语义被重新定义了——敏感指令可以由 VM-entry/VM-exit 机制自动陷入到 Hypervisor,而不必走传统异常路径。
这两个模式拿到的实际 CPL 可能还是 0,但硬件已经区分了“我是真内核”还是“我是客户机内核”。这是一次大的架构变化,本质上是把“虚拟化”这一特例从传统的四环模型里拎出来,专门加了一条硬件的快车道。
做云计算的同事如果遇到“我的用户态程序一跑某些指令就整个虚拟机出问题”,往往就是没意识到 Hypervisor 对某些指令的拦截行为,例如检查 CPUID 某些 leaf、尝试访问某些特权 MSR 时触发了 VM-exit。这不是普通 Ring 3 程序踩坑的日常,但做 Hypervisor 的同事天天见。
5.2 微内核架构里的“特权级”新解读
还有一条线是微内核(Microkernel)设计:例如 L4 系列和 seL4。这类系统放弃“驱动全在 Ring 0”的模型,把设备驱动放到用户态进程,内核只提供 IPC 和调度。这时你发现,Ring 0 和 Ring 3 不再是绝对的“判断对错”的层级,而变成一种资源调度和错误隔离的边界。驱动崩了,不会拉垮整个内核,而是由一个用户态服务进程重启驱动。
但微内核引入了一个新问题:用户态驱动需要访问硬件、需要操作中断,怎么办?答案是依赖内核转发,或者在某些架构上把部分 IO 权限、中断绑定能力通过 capability(能力句柄)下放。这回到前文说的 IO 位图和 IOPL 机制——Ring 3 的驱动可以用部分硬件能力,但必须由内核“精准授权”。这种思想恰恰是 x86 特权级分级最早没细化的能力边界,到了微内核时代又被重新挖出来放大。
5.3 容器到底有没有碰特权级
容器(Container)跟虚拟机的底层完全不同。容器共享宿主内核,所以容器里的进程仍然跑在宿主内核的 CPL=3 上,容器隔离靠的完全是软件层面的 namespaces、cgroups、seccomp 等。这意味着在特权级这件事上,容器并没有多一层“硬件围栏”。
这也是为什么跑 Docker 时,你会看到运行时默认禁用某些 sysctl、限制某些系统调用——不是内核不让你调,而是容器运行时担心你在用户态借内核的漏洞突破 namespace 隔离。根本上,它依赖内核自身的安全性,和 VM 靠硬件隔离在风险面上差了一个量级。
我们做底层安全时,判断一个环境的安全边界,永远要问一句:到底是硬件强制隔离,还是软件约定隔离?硬件做了特权级检查,软件只能把规则捡起来遵守,但没法绕过硬件。软件约定只要有一处破洞,整个边界就形同虚设。
6. 特权级漏洞与安全:一票“听起来很高级”的漏洞全是特权级的战场
6.1 越权漏洞的本质:都是让低特权代码碰到了高特权数据
安全圈里大面积出现的所谓“内核提权漏洞”,绝大多数都围绕同一个主题:一个 Ring 3 的进程,竟然能通过某种方式触达 Ring 0 才能访问的数据或指令。常见套路有:
- 内核存在一个可被用户态触发的 bug,比如越界写或读,导致用户态能以 Ring 0 身份执行一段攻击者控制的代码。
- 某个系统调用把用户指针当作内核指针用,绕过访问权限检查。
- 硬件层面存在预测执行的侧信道,例如 Meltdown,本质是 CPU 在乱序/预测执行时提前访问了 Supervisor-only 的内存页,然后通过缓存时间侧信道把数据带出来。
这些东西,对普通开发者的警示意义在于:别以为你的程序在 Ring 3 就万事大吉,也别以为你的数据页在 Supervisor 域就绝对安全。硬件和软件的合作防线,不是铁板一块。
6.2 Meltdown/Spectre 视角里的特权级
Meltdown 的关键在于:CPU 在执行时可能会“越权读取”某些地址,然后虽然最终指令结果会被丢弃,但缓存的状态已经被污染。攻击者通过测量自己的某个数组访问时间,可以推断出内存内容。它绕过了页表的 U/S 检查吗?不是绕过,而是利用了“CPU 在检查权限前就触发了侧信道读取”的乱序执行窗口。
这类漏洞给业界的教训是:硬件层面每一层优化(乱序执行、预测执行)都可能无意间挑战硬件的安全边界。此后操作系统大规模更新页表隔离(KPTI,Kernel Page Table Isolation)——把用户进程页表里的内核地址映射移除,这样用户态去访问一个内核虚拟地址时,页表里压根没有映射,页表这一层就把它挡死。这就是典型的“在软件层面修复硬件隐患”:既然硬件上页表的 U/S 检查在乱序窗口里靠不住,那就干脆别给你这个页表项。
6.3 给开发者的三条安全建议(实战向)
第一,不要关闭内核态页表隔离,哪怕牺牲一点性能。尤其伺服类、网关类、虚拟化宿主机这类设备,KPTI 的收益远大于那点 syscall 开销。
第二,驱动和内核模块不要信任用户态指针。要用copy_from_user/copy_to_user这类安全接口去拷贝,而不是直接把用户地址解引用。内核里解引用用户地址,在 SMAP/SMEP 开启的平台上可能直接触发异常,而在老平台上就可能变成可利用的漏洞。
第三,关注 MSR 和调试相关寄存器的访问权限,做安全研究时尤其如此。如果某条指令能让你从 Ring 3 触发 Ring 0 的路径,并且错误处理不完善,这往往就是一条提权链的起点。
7. 实操环节:写代码验证特权级模型(附排查方案)
7.1 实验一:在 Ring 0 与 Ring 3 之间看段寄存器
写一个最简单的内核模块或者用 QEMU 调试,你想直观感受特权级切换,最直接的办法是看每段代码运行时 CS 的 RPL 值。用info registers看 CS,低位就是等级。典型输出里,Ring 0 下 CS=0x08(二进制 01000,RPL=0),Ring 3 下 CS=0x23(二进制 100011,RPL=3)。
没有实际内核环境的话,也能靠用户态读cs段寄存器值确认环境:在 64 位 Linux 用户态,你可以写一小段内联汇编读取 CS,基本观察到的值就是 0x33(RPL=3)。测试代码大致是:
#include <stdio.h> static unsigned short get_cs(void) { unsigned short sel; __asm__ __volatile__("mov %%cs, %0" : "=r"(sel)); return sel; } int main(void) { printf("CS = 0x%x (RPL=%u)\n", get_cs(), get_cs() & 3); return 0; }编译运行,你会看到 RPL=3。这是一个非常直观的“我在用户态”的硬证据。如果在调试器里附加到内核,再看内核入口时同一寄存器,RPL 就变成了 0。这套验证方法适合入门的同事建立第一手体感。
7.2 实验二:尝试执行特权指令,验证 #GP
在用户态写一段尝试执行LGDT或HLT的代码,运行时会收到 SIGSEGV 或者触发非法指令异常。原因是 CPU 在取指/执行阶段就会做特权级检查,LGDT 要求 CPL=0,当前 CPL=3,直接拒绝。
#include <stdio.h> int main(void) { printf("try execute privileged instruction...\n"); __asm__ __volatile__("hlt"); return 0; }注意:单纯hlt在用户态大概率直接崩掉或触发#GP,个别环境下可能需要配合信号处理才不整个进程退出。这里的意义在于证明“用户态无法执行特权指令”不是软件约定,是硬件出手了。
7.3 常见的排查问题速查
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 用户态程序访问内核地址时 segfault | 页表里 Supervisor-only 页不可访问 | 用调试器查看页表项的 U/S 位 |
| 内核态访问用户态指针时崩溃 | 未用 copy_from_user,或 SMAP/SMEP 已开启 | 检查 CR4 寄存器里 SMAP/SMEP 位 |
| int 0x80 在 64 位系统不好使 | 需要在兼容模式下处理,或系统用 syscall 快路径 | 检查 syscall 入口配置 |
| 某些寄存器用户态能读不能写 | 架构规定只读/只在特定 CPL 可写 | 查 arch manual 对应寄存器权限 |
| VMM 环境里执行指令卡死 | 客户机触发 VM-exit,hypervisor 拦截了某些行为 | 用 VMCS 追踪 VM-exit reason |
排查时,最常用的就是调试器:看 CR0/CR4 里保护开关有没有打开,看 GDT/IDT 里描述符的 DPL,看 CS 的 RPL。这些在主流调试器和 QEMU monitor 下都能直接读出来。每次出现“为什么我的驱动能访问这个地址但普通程序不行”之类的问题,十有八九最后都会落到 U/S 位或者 DPL 的某一步 CPU 检查。
8. 从 Ring 到未来:为什么这套四环模型至今仍是地基中的地基
看到这里,你会发现一个很有意思的事实:过去几十年微架构发生了天翻地覆的变化,乱序执行、超线程、大小核、虚拟化扩展,但 Ring 0~Ring 3 这个框架本身没有动摇。它不是最漂亮的设计,甚至有些冗余,但它足够简单、足够硬件化、足够可信。做系统的人,最怕的就是保护机制本身有软肋,因为一旦保护机制不可信,上面加再多安全软件都是空中楼阁。
我个人这些年在搞底层和性能优化时的一个体会是:所有关于 CPU 权限的讨论,最后都指向同一个问题——谁有能力越过硬件设立的边界。无论是内核提权、虚拟机逃逸、固件漏洞还是各类侧信道,本质都是某些越界访问混过了硬件检查。理解 Ring 特权级,本质上就是在建立一张“CPU 权力地图”,知道什么东西在哪一层、谁可以跨层、跨层时走哪条门,这会让你在往后看任何底层安全问题、系统设计问题时,都有一种非常牢固的代入感。
如果是刚接触底层这块,建议先按文中前几节的思路把 CS 的 RPL、页表的 U/S 位、描述符的 DPL 三者搞清楚,再动手做一遍那两个小实验。等你自己写出第一个需要特权级切换的代码时,再回头看这篇文章,会有一种“硬件的护栏其实设计得很体贴”的感觉。