您可能凭直觉知道应用程序在 Intel x86 计算机中的功能有限,并且只有操作系统代码才能执行某些任务,但是您知道它是如何工作的吗?这篇文章介绍了 x86权限级别,操作系统和 CPU 合力限制用户模式程序可以做什么的机制。有四个特权级别,编号为 0(最高特权)到 3(最低特权),并且受保护的三个主要资源是:内存、I/O 端口和执行某些机器指令的能力。在任何给定的时间,x86 CPU 都在特定的特权级别运行,这决定了代码可以做什么和不可以做什么。这些权限级别通常被描述为保护环,最里面的环对应于最高权限。大多数现代 x86 内核仅使用两个特权级别,0 和 3:

docer 的CPU 权重设置鸡 cpu权限_描述符


x86 保护环

大约有 15 条机器指令,在几十条机器指令中,被 CPU 限制为响零。许多其他人对其操作数有限制。如果在用户模式下允许,这些指令可能会破坏保护机制或以其他方式煽动混乱,因此它们被保留给内核。尝试在零环之外运行它们会导致一般保护异常,例如当程序使用无效内存地址时。同样,对内存和 I/O 端口的访问也受到权限级别的限制。但是,我们看看保护机制之前,让我们来看看究竟的CPU如何跟踪当前的特权级别,其中涉及的段选择从以前的帖子。他们来了:

docer 的CPU 权重设置鸡 cpu权限_寄存器_02


段选择器 - 数据和代码

数据段选择器的全部内容由代码直接加载到ss(堆栈段寄存器)和ds(数据段寄存器)等各种段寄存器中。这包括请求的权限级别 (RPL) 字段的内容,我们稍后会处理其含义。然而,代码段寄存器 (cs) 是神奇的。首先,它的内容不能直接由加载指令(如 mov)设置,而只能由改变程序执行流程的指令(如调用)设置。其次,对我们来说很重要的是,cs 拥有一个由 CPU 本身维护的当前特权级别(CPL) 字段,而不是可以通过代码设置的 RPL 字段。代码段寄存器中的这个 2 位 CPL 字段总是等于CPU 的当前权限级别。英特尔文档在这个事实上有点摇摆不定,有时在线文档会混淆这个问题,但这是硬性规定。在任何时候,无论 CPU 中发生了什么,查看 cs 中的 CPL 都会告诉您正在运行的特权级别代码。

请记住,CPU 权限级别与操作系统用户无关。无论您是 root、管理员、访客还是普通用户,都没有关系。所有的用户代码中环3中运行,并在ring 0所有内核代码的运行,无论所代表的代码运行的OS用户。有时某些内核任务可以推送到用户模式,例如 Windows Vista 中的用户模式设备驱动程序,但这些只是为内核执行工作的特殊进程,通常可以被终止而不会产生重大后果。

由于对内存和 I/O 端口的访问受限,用户模式在不调用内核的情况下几乎不能对外界做任何事情。它不能打开文件、发送网络数据包、打印到屏幕或分配内存。用户进程在由零环之神设置的严格受限的沙箱中运行。这就是为什么这是不可能的,按照设计,进程在其存在后泄漏内存或在退出后保留打开的文件。所有控制这些事情的数据结构——内存、打开的文件等——都不能被用户代码直接触及;一旦进程完成,沙箱就会被内核拆除。这就是为什么我们的服务器可以有 600 天的正常运行时间 - 只要硬件和内核不坏,东西就可以永远运行。这也是 Windows 95 / 98 崩溃如此严重的原因:不是因为“M$ 糟糕透顶”,而是因为出于兼容性原因,用户模式可以访问重要的数据结构。这在当时可能是一个很好的权衡,尽管成本很高。

CPU 在两个关键点保护内存:加载段选择器时和使用线性地址访问内存页时。因此,保护反映了涉及分段和分页的内存地址转换。当加载数据段选择器时,会进行以下检查:

docer 的CPU 权重设置鸡 cpu权限_加载_03


x86 段保护

由于更高的数字意味着更少的特权,上面的 MAX() 选择 CPL 和 RPL 中特权最低的,并将其与描述符特权级别 (DPL) 进行比较。如果 DPL 高于或等于,则允许访问。RPL 背后的想法是允许内核代码使用降低的权限加载段。例如,您可以使用 3 的 RPL 来确保给定操作使用用户模式可访问的段。堆栈段寄存器 ss 是个例外,CPL、RPL 和 DPL 三个必须完全匹配。

事实上,段保护几乎无关紧要,因为现代内核使用平面地址空间,用户模式段可以到达整个线性地址空间。当线性地址转换为物理地址时,在分页单元中进行有用的内存保护。每个内存页都是一个由页表条目描述的字节块包含两个与保护相关的字段:主管标志和读/写标志。supervisor 标志是内核使用的主要 x86 内存保护机制。当它打开时,页面不能从环 3 访问。虽然读/写标志对于强制执行权限没有那么重要,但它仍然有用。加载进程时,存储二进制图像(代码)的页面被标记为只读,从而在程序尝试写入这些页面时捕获一些指针错误。该标志也用于实现写时复制当一个进程在 Unix 中分叉时。分叉后,父页面被标记为只读并与分叉的孩子共享。如果任一进程尝试写入该页面,则处理器会触发故障并且内核知道复制该页面并将其标记为读/写以供写入进程使用。

最后,我们需要一种让 CPU 在特权级别之间切换的方法。如果 ring 3 代码可以将控制权转移到内核中的任意位置,那么很容易通过跳入错误(对?)的地方来颠覆操作系统。受控转移是必要的。这是通过完成栅极描述符,并经由SYSENTER操作说明。门描述符是系统类型的段描述符,分为四种子类型:调用门描述符、中断门描述符、陷阱门描述符和任务门描述符。调用门提供了一个内核入口点,可以与普通的 call 和 jmp 指令一起使用,但它们用得不多,所以我将忽略它们。任务门也不是那么热(在 Linux 中,它们仅用于由内核或硬件问题引起的双重故障)。

剩下两个更有趣的:中断门和陷阱门,它们用于处理硬件中断(例如,键盘、计时器、磁盘)和异常(例如,页面错误、除以零)。我将两者都称为“中断”。这些门描述符存储在中断描述符表(IDT) 中。每个中断都分配了一个介于 0 和 255 之间的数字,称为vector,处理器在确定处理中断时使用哪个门描述符时,将其用作 IDT 的索引。中断门和陷阱门几乎相同。它们的格式以及中断发生时强制执行的权限检查如下所示。我为 Linux 内核填写了一些值以使事情具体化。

docer 的CPU 权重设置鸡 cpu权限_加载_04


具有权限检查的中断描述符

DPL 和门中的段选择器都调节访问,而段选择器和偏移量一起确定了中断处理程序代码的入口点。内核通常对这些门描述符中的内核代码段使用段选择器。中断永远不能将控制权从较高特权的环转移到较低特权的环。特权必须保持不变(当内核本身被中断时)或被提升(当用户模式代码被中断时)。在任何一种情况下,生成的 CPL 都将等于目标代码段的 DPL;如果 CPL 发生变化,也会发生堆栈切换。如果中断是由代码通过像int n这样的指令触发的,再进行一项检查:门 DPL 必须与 CPL 具有相同或更低的特权。这可以防止用户代码触发随机中断。如果这些检查失败——你猜对了——一般保护异常就会发生。所有 Linux 中断处理程序最终都在零环中运行。

在初始化过程中,Linux 内核首先在setup_idt()中设置一个忽略所有中断的 IDT 。然后它使用include/asm-x86/desc.h 中的函数来充实arch/x86/kernel/traps_32.c 中的常见 IDT 条目。在 Linux 中,名称中带有“system”的门描述符可从用户模式访问,其 set 函数使用的 DPL 为 3。“系统门”是用户模式可访问的 Intel 陷阱门。否则,术语匹配。然而,硬件中断门不是在这里设置的,而是在适当的驱动程序中设置的。

用户模式可以访问三个门:向量 3 和 4 分别用于调试和检查数字溢出。然后为SYSCALL_VECTOR设置系统门,x86 架构为 0x80。这是该机制的过程控制转移到内核,做一个系统调用,并早在我申请的是“INT 0x80的”虚荣车牌:)一天。从 Pentium Pro 开始,sysenter引入指令是为了更快地进行系统调用。它依赖于专用 CPU 寄存器来存储内核系统调用处理程序的代码段、入口点和其他花絮。当执行 sysenter 时,CPU 不进行权限检查,立即进入 CPL 0 并将新值加载到代码和堆栈寄存器(cs、eip、ss 和 esp)中。只有环零可以加载 sysenter 设置寄存器,这是在enable_sep_cpu() 中完成的。

最后,当需要返回 ring 3 时,内核发出iret或sysexit指令分别从中断和系统调用返回,从而离开 ring 0 并恢复执行 CPL 为 3 的用户代码。Vim告诉我我' m 接近 1,900 字,所以 I/O 端口保护是另一天。我们的 x86 环和保护之旅到此结束。谢谢阅读!