我一直在学习有关 Windows 中驱动程序开发的基础知识,我一直在寻找术语Ring 0和Ring 3。这些指的是什么?它们与内核模式和用户模式相同吗?
2 回答
Linux x86 环使用概述
了解环在 Linux 中的使用方式将使您对它们的设计目的有一个很好的了解。
在 x86 保护模式下,CPU 始终处于 4 个环之一。Linux内核只使用0和3:
- 0 表示内核
- 3 用户
这是内核与用户空间的最严格和最快速的定义。
为什么 Linux 不使用环 1 和 2:CPU 特权环:为什么不使用环 1 和 2?
当前的戒指是如何确定的?
当前环由以下组合选择:
全局描述符表:内存中的 GDT 条目表,每个条目都有一个
Privl
对环进行编码的字段。LGDT 指令将地址设置为当前描述符表。
段寄存器 CS、DS 等,它们指向 GDT 中条目的索引。
例如,
CS = 0
表示 GDT 的第一个条目当前对正在执行的代码处于活动状态。
每个戒指能做什么?
CPU 芯片的物理构造使得:
ring 0 可以做任何事情
ring 3 不能运行多个指令并写入多个寄存器,最值得注意的是:
不能改变自己的戒指!否则,它可能会将自己设置为 ring 0,而 ring 将无用。
也就是说,不能修改当前段描述符,它决定了当前的环。
无法修改页表:x86 分页如何工作?
换句话说,不能修改CR3寄存器,而分页本身阻止了对页表的修改。
出于安全/易于编程的原因,这可以防止一个进程看到其他进程的内存。
无法注册中断处理程序。这些是通过写入内存位置来配置的,这也可以通过分页来防止。
处理程序在环 0 中运行,并且会破坏安全模型。
也就是说,不能使用 LGDT 和 LIDT 指令。
不能像
in
and那样执行 IO 指令out
,因此可以进行任意硬件访问。否则,例如,如果任何程序可以直接从磁盘读取,文件权限将毫无用处。
更准确地说,感谢Michael Petch:操作系统实际上可以在 ring 3 上允许 IO 指令,这实际上是由任务状态段控制的。
环 3 不可能允许自己这样做,如果它一开始就没有的话。
Linux总是不允许它。另请参阅:为什么 Linux 不通过 TSS 使用硬件上下文切换?
程序和操作系统如何在环之间转换?
当 CPU 开启时,它开始运行 ring 0 中的初始程序(很好,但它是一个很好的近似值)。您可以将这个初始程序视为内核(但它通常是一个引导加载程序,然后调用仍在 ring 0 中的内核)。
当用户态进程希望内核为它做一些事情(例如写入文件)时,它会使用一条指令来生成中断,例如向内核发出
int 0x80
或syscall
信号。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
发生这种情况时,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 设置,可以在不杀死主机的情况下进行尝试。
内核模块的缺点是其他 kthread 正在运行,可能会干扰您的实验。但理论上你可以用你的内核模块接管所有的中断处理程序并拥有系统,这实际上是一个有趣的项目。
负环
虽然英特尔手册中实际上并未提及负环,但实际上存在比环 0 本身具有更多功能的 CPU 模式,因此非常适合“负环”名称。
一个例子是虚拟化中使用的管理程序模式。
有关详细信息,请参阅:
- https://security.stackexchange.com/questions/129098/what-is-protection-ring-1
- https://security.stackexchange.com/questions/216527/ring-3-exploits-and-existence-of-other-rings
手臂
在 ARM 中,环被称为异常级别,但主要思想保持不变。
ARMv8 中存在 4 个异常级别,常用的有:
EL0:用户空间
EL1:内核(ARM 术语中的“主管”)。
用
svc
指令(SuperVisor Call)输入,以前称为swi
统一汇编,是用来进行Linux系统调用的指令。你好世界 ARMv8 示例:你好.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
在 Ubuntu 16.04 上使用 QEMU 进行测试:
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
这是一个注册 SVC 处理程序并执行 SVC 调用的具体裸机示例。
-
输入
hvc
指令(HyperVisor Call)。管理程序之于操作系统,就像操作系统之于用户空间。
例如,Xen 允许您在同一系统上同时运行多个操作系统(如 Linux 或 Windows),并且它将操作系统彼此隔离以确保安全性和易于调试,就像 Linux 对用户级程序所做的那样。
管理程序是当今云基础架构的关键部分:它们允许多台服务器在单个硬件上运行,使硬件使用率始终接近 100% 并节省大量资金。
例如,AWS 一直使用 Xen,直到 2017 年它转向 KVM 成为新闻。
EL3:又一个层次。待办事项示例。
输入
smc
指令(安全模式调用)
ARMv8 架构参考模型 DDI 0487C.a - 第 D1 章 - AArch64 系统级程序员模型 - 图 D1-1 很好地说明了这一点:
随着ARMv8.1 虚拟化主机扩展 (VHE)的出现,ARM 的情况发生了一些变化。这个扩展允许内核有效地在 EL2 中运行:
创建 VHE 是因为 KVM 等 Linux 内核内虚拟化解决方案已经超越 Xen(例如,参见上面提到的 AWS 向 KVM 的迁移),因为大多数客户端只需要 Linux VM,并且您可以想象,它们都在一个单一的项目中,KVM 比 Xen 更简单并且可能更高效。因此,现在主机 Linux 内核在这些情况下充当管理程序。
从图中我们可以看到,当寄存器的位E2H
HCR_EL2
等于 1 时,VHE 被启用,并且:
- Linux 内核在 EL2 而不是 EL1 中运行
- 当 时
HCR_EL2.TGE == 1
,我们是一个常规的主机用户态程序。使用sudo
可以像往常一样破坏主机。 - 当
HCR_EL2.TGE == 0
我们是来宾操作系统时(例如,当您在主机 Ubuntu 内的 QEMU KVM 内运行 Ubuntu 操作系统时。除非存在 QEMU/主机内核错误,否则sudo
不会破坏主机。
请注意 ARM 可能是出于事后诸葛亮的好处,它对特权级别的命名约定比 x86 更好,而不需要负级别:0 是较低的,3 是最高的。较高级别往往比较低级别更频繁地创建。
当前的EL可以通过MRS
指令查询:当前执行模式/异常级别等是什么?
ARM 不需要所有异常级别都存在,以允许不需要该功能以节省芯片面积的实现。ARMv8“异常级别”说:
一个实现可能不包括所有的异常级别。所有实现必须包括 EL0 和 EL1。EL2 和 EL3 是可选的。
例如 QEMU 默认为 EL1,但可以使用命令行选项启用 EL2 和 EL3:qemu-system-aarch64 在模拟 a53 开机时输入 el1
在 Ubuntu 18.10 上测试的代码片段。
英特尔处理器(x86 和其他)允许应用程序有限的功能。为了限制(保护)关键资源,如 IO、内存、端口等,与操作系统(在本例中为 Windows)连接的 CPU 提供了分别映射到内核模式和用户模式的特权级别(0 是最高特权,3 是最低特权)。
因此,操作系统在环 0 中运行内核代码——CPU 提供的最高权限级别(0)——在环 3 中运行用户代码。
有关更多详细信息,请参阅http://duartes.org/gustavo/blog/post/cpu-rings-privilege-and-protection/