操作系统中的Ring 0和Ring 3是什么?

69

我一直在学习Windows驱动程序开发的基础知识,但是我经常会遇到术语 Ring 0Ring 3。 它们指的是什么?它们与 内核模式用户模式 是同样的东西吗?


1
是的,它们与内核模式和用户模式相同,在某些版本的Windows中,环1和环2实际上用于设备驱动程序。 - dwarduk
2个回答

146

Linux x86环使用概述

了解Linux中如何使用环将会让您对它们的设计有一个很好的理解。

在x86保护模式下,CPU始终处于4个环之一。Linux内核仅使用0和3:

  • 0用于内核
  • 3用于用户

这是内核与用户空间最严格的定义。

为什么Linux不使用1和2环:CPU特权环:为什么不使用1和2环?

当前环是如何确定的?

当前环由以下组合选择:

  • 全局描述符表: 一个内存中的表格包含GDT条目,每个条目都有一个字段Privl,用于编码所在的特权级。

    LGDT指令将地址设置为当前描述符表。

    参见: http://wiki.osdev.org/Global_Descriptor_Table

  • 段寄存器CS、DS等,它们指向GDT中一个条目的索引。

    例如,CS = 0表示GDT的第一个条目当前对执行代码处于活动状态。

每个特权级可以做什么?

CPU芯片是物理上构建的,因此:

  • ring 0 可以做任何事情

  • ring 3 不能运行几个指令并写入几个寄存器,最明显的是:

    • 不能更改自己的环!否则,它可以将自己设置为 ring 0,而环将毫无用处。

      换句话说,不能修改当前段描述符,该描述符确定了当前环。

    • 不能修改页表:x86 分页是如何工作的?

      换句话说,不能修改 CR3 寄存器,而分页本身防止了对页表的修改。

      这样做是为了安全性/编程方便性,防止一个进程看到其他进程的内存。

    • 不能注册中断处理程序。这些是通过写入内存位置来配置的,这也被分页所阻止。

      处理程序在 ring 0 中运行,会破坏安全模型。

      换句话说,不能使用 LGDT 和 LIDT 指令。

    • 不能执行 IO 指令,例如 inout,因此无法进行任意硬件访问。

      否则,例如,文件权限将是无用的,如果任何程序都可以直接从磁盘读取。

      更准确地说,感谢Michael Petch:操作系统实际上可以允许 ring 3 上的 IO 指令,这实际上是由任务状态段控制的。

      不可能的是,ring 3 给自己授权执行 IO 操作,如果它在第一次没有该权限。

      Linux 总是禁止它。另请参见:为什么 Linux 不使用 TSS 进行硬件上下文切换?

程序和操作系统如何在不同的特权级之间进行转换?

  • 当CPU被打开时,它会开始在特权级0中运行初始程序(这是一个很好的近似,可以认为这个初始程序是内核,但通常是引导程序然后调用仍在特权级0中的内核)。

  • 当用户进程想要让内核为其执行某些操作(例如写入文件)时,它使用生成中断的指令,例如int 0x80syscall来向内核发出信号。x86-64 Linux系统调用hello world示例:

.data
hello_world:
    .ascii "hello world\n"
    hello_world_len = . - hello_world
.text
.global _start
_start:
    /* write */
    mov $1, %rax
    mov $1, %rdi
    mov $hello_world, %rsi
    mov $hello_world_len, %rdx
    syscall

    /* exit */
    mov $60, %rax
    mov $0, %rdi
    syscall

编译并运行:

as -o hello_world.o hello_world.S
ld -o hello_world.out hello_world.o
./hello_world.out

GitHub upstream

当这种情况发生时,CPU会调用内核在启动时注册的中断回调处理程序。这里有一个具体的裸机示例,它注册了一个处理程序并使用它

此处理程序在ring 0中运行,决定内核是否允许此操作,执行操作,并在ring 3中重新启动用户程序。x86_64

  • 当使用exec系统调用(或者当内核启动/init时),内核会准备新用户空间进程的寄存器和内存,然后跳转到入口点并将CPU切换到ring 3。

  • 如果程序尝试做一些不好的事情,比如写入禁止的寄存器或内存地址(由于分页),CPU也会在ring 0中调用一些内核回调处理程序。

    但由于用户空间行为不当,内核可能会这次杀掉该进程,或者用信号发出警告。

  • 当内核启动时,它设置了一个硬件时钟,具有一定的固定频率,可以定期生成中断。

    这个硬件时钟生成运行在ring 0上的中断,并允许其安排要唤醒的用户空间进程。

    这样,即使进程没有进行任何系统调用,调度也可以发生。

多个环的目的是什么?

将内核和用户空间分离有两个主要优点:

  • 更容易制作程序,因为您更加确定一个程序不会干扰另一个程序。例如,一个用户空间进程不必担心由于分页而覆盖另一个程序的内存,也不必担心为另一个进程设置硬件处于无效状态。
  • 更安全。例如,文件权限和内存隔离可以防止黑客应用程序读取您的银行数据。当然,这假设您信任内核。

如何进行操作?

我创建了一个裸机设置,应该是直接操作环的好方法: https://github.com/cirosantilli/x86-bare-metal-examples

不幸的是,我没有耐心创建用户空间示例,但我已经完成了分页设置,因此用户空间应该是可行的。我很想看到一个拉取请求。

另外,Linux内核模块在ring 0中运行,因此您可以使用它们尝试特权操作,例如读取控制寄存器:如何从程序中访问控制寄存器cr0、cr2、cr3?获得分段错误

这里有一个方便的QEMU + Buildroot设置,可以在不破坏主机的情况下尝试它。

内核模块的缺点是其他kthreads正在运行,可能会干扰您的实验。但理论上,您可以使用内核模块接管所有中断处理程序并拥有系统,这将是一个有趣的项目。

负环

虽然Intel手册中实际上没有提到负环,但实际上有CPU模式比ring 0本身具有更多的功能,因此很适合“负环”名称。

一个例子是虚拟化中使用的超级用户模式。
更多详细信息请参见:

ARM

在ARM中,环被称为异常级别(Exception Levels),但主要思想仍然相同。

ARMv8存在4个异常级别,通常用作:

  • EL0: userland

  • EL1: kernel ("supervisor" in ARM terminology).

    Entered with the svc instruction (SuperVisor Call), previously known as swi before unified assembly, which is the instruction used to make Linux system calls. Hello world ARMv8 example:

    hello.S

    .text
    .global _start
    _start:
        /* write */
        mov x0, 1
        ldr x1, =msg
        ldr x2, =len
        mov x8, 64
        svc 0
    
        /* exit */
        mov x0, 0
        mov x8, 93
        svc 0
    msg:
        .ascii "hello syscall v8\n"
    len = . - msg
    

    GitHub upstream.

    Test it out with QEMU on Ubuntu 16.04:

    sudo apt-get install qemu-user gcc-arm-linux-gnueabihf
    arm-linux-gnueabihf-as -o hello.o hello.S
    arm-linux-gnueabihf-ld -o hello hello.o
    qemu-arm hello
    

    Here is a concrete baremetal example that registers an SVC handler and does an SVC call.

  • EL2: hypervisors, for example Xen.

    Entered with the hvc instruction (HyperVisor Call).

    A hypervisor is to an OS, what an OS is to userland.

    For example, Xen allows you to run multiple OSes such as Linux or Windows on the same system at the same time, and it isolates the OSes from one another for security and ease of debug, just like Linux does for userland programs.

    Hypervisors are a key part of today's cloud infrastructure: they allow multiple servers to run on a single hardware, keeping hardware usage always close to 100% and saving a lot of money.

    AWS for example used Xen until 2017 when its move to KVM made the news.

  • EL3: yet another level. TODO example.

    Entered with the smc instruction (Secure Mode Call)

ARMv8架构参考模型DDI 0487C.a - 第D1章 - AArch64系统级程序员模型 - 图D1-1生动地说明了这一点:

enter image description here

ARM随着ARMv8.1虚拟化主机扩展(VHE)的出现,情况有所改变。该扩展允许内核高效地在EL2中运行:

enter image description here

VHE的创建是因为像KVM这样的内核虚拟化解决方案已经在Linux内部得到了普及(例如,上面提到的AWS转向KVM),因为大多数客户只需要Linux VM,并且可以想象,作为一个单一项目,KVM比Xen更简单、潜在更有效率。因此,在这些情况下,主机Linux内核充当虚拟机监视器。
从图像中我们可以看到,当寄存器HCR_EL2的位E2H等于1时,VHE被启用,然后:
  • Linux内核运行在EL2而不是EL1
  • HCR_EL2.TGE == 1 时,我们是一个常规的主机用户程序。使用 sudo 可以像往常一样破坏主机。
  • HCR_EL2.TGE == 0 时,我们是一个客户操作系统(例如,当您 在主机Ubuntu内部运行QEMU KVM中的Ubuntu OS)。除非存在QEMU / host内核错误,否则无法通过执行sudo来破坏主机。

请注意,ARM可能由于事后认识到的好处,比x86有更好的特权级别命名约定,而无需使用负级别:0为最低,3为最高。比较常见的是创建更高的级别。

可以使用MRS指令查询当前EL:当前执行模式/异常级别等是什么?

ARM不要求所有的异常级别都存在,以允许那些不需要该功能的实现来节省芯片面积。ARMv8“异常级别”规定: “某些实现可能不包括所有异常级别。所有实现必须包括EL0和EL1。EL2和EL3是可选的。”
例如,QEMU默认为EL1,但可以使用命令行选项启用EL2和EL3:qemu-system-aarch64 entering el1 when emulating a53 power up 在Ubuntu 18.10上测试的代码片段。

6
哇,多好的回答! - cfs
4
非常详细的回答,学到了很多! - Moritz

14

英特尔处理器(x86和其他)允许应用程序拥有有限的权限。为了限制(保护)关键资源,如IO、内存、端口等,CPU与操作系统(在此案例中为Windows)一起提供特权级别(0表示最高特权,3表示最低),分别对应内核模式和用户模式。

因此,操作系统在最高特权级别下(0环)运行内核代码,并在第三个特权级别(3环)下运行用户代码。

有关更多详细信息,请参见http://duartes.org/gustavo/blog/post/cpu-rings-privilege-and-protection/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接