当内核代表用户程序(即系统调用)执行时,是否使用内核空间?或者它是所有内核线程(例如调度程序)的地址空间?
如果是第一个,那么是否意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?另外,在这种情况下,内核如何使用高内存,因为高内存中的页面将映射到哪个虚拟内存地址,因为 1GB 的内核空间将被逻辑映射?
答案1
当内核代表用户程序(即系统调用)执行时,是否使用内核空间?或者它是所有内核线程(例如调度程序)的地址空间?
是的,是的。
在我们进一步讨论之前,我们应该先说明一下关于内存的问题。
内存分为两个不同的区域:
- 用户空间,这是正常用户进程运行的一组位置(即除内核之外的所有位置)。内核的作用是管理在此空间中运行的应用程序,以免相互干扰以及与机器干扰。
- 内核空间,这是内核的代码和数据存储的位置,并在其下执行。
运行在用户空间下的进程只能访问内存的有限部分,而内核可以访问所有内存。进程也运行在用户空间不可以访问内核空间。用户空间进程可以只访问内核的一小部分通过内核公开的接口 -系统调用。如果进程执行系统调用,则会将软件中断发送到内核,然后内核调度适当的中断处理程序,并在处理程序完成后继续其工作。
内核空间代码具有在“内核模式”下运行的属性,这(在典型的桌面 -x86- 计算机中)是您所需要的调用在ring 0下执行的代码。通常在x86架构中,有4个保护环。 Ring 0(内核模式)、Ring 1(可能由虚拟机管理程序或驱动程序使用)、Ring 2(可能由驱动程序使用,但我对此不太确定)。 Ring 3 是典型应用程序运行的环境。它是权限最小的环,在其上运行的应用程序可以访问处理器指令的子集。环 0(内核空间)是特权最高的环,可以访问所有机器指令。举个例子,“普通”应用程序(如浏览器)不能使用 x86 汇编指令lgdt
来加载全局描述符表,也hlt
不能停止处理器。
如果是第一个,那么是否意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?另外,在这种情况下,内核如何使用高内存,因为高内存中的页面将映射到哪个虚拟内存地址,因为 1GB 的内核空间将被逻辑映射?
对于这个问题的答案,请参考wag 的出色回答到Linux 上的高内存和低内存是什么?。
答案2
CPU环是最明显的区别
在 x86 保护模式下,CPU 始终处于 4 个环之一。 Linux内核只使用0和3:
- 0 表示内核
- 3 对于用户
这是内核与用户态的最严格和最快速的定义。
为什么 Linux 不使用环 1 和环 2:https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used
当前环是如何确定的?
当前环是通过以下组合选择的:
全局描述符表:GDT条目的内存表,每个条目都有一个对
Privl
环进行编码的字段。LGDT指令将地址设置为当前描述符表。
段寄存器CS、DS等,它们指向GDT中某个条目的索引。
例如,
CS = 0
表示 GDT 的第一个条目当前对于执行代码处于活动状态。
每个戒指能做什么?
CPU 芯片的物理构建使得:
环0可以做任何事情
环 3 无法运行多个指令并写入多个寄存器,最值得注意的是:
无法改变自己的戒指!否则,它可以将自己设置为ring 0,而rings将毫无用处。
换句话说,无法修改当前的段描述符,它决定了当前的环。
无法修改页表:https://stackoverflow.com/questions/18431261/how-does-x86-paging-work
换句话说,不能修改CR3寄存器,并且分页本身阻止了页表的修改。
出于安全性/易于编程的原因,这可以防止一个进程看到其他进程的内存。
无法注册中断处理程序。这些是通过写入内存位置来配置的,这也可以通过分页来防止。
处理程序在环 0 中运行,并且会破坏安全模型。
换句话说,不能使用LGDT和LIDT指令。
in
不能执行像和 之类的 IO 指令out
,因此可以进行任意硬件访问。否则,例如,如果任何程序可以直接从磁盘读取,则文件权限将毫无用处。
更准确地说,感谢迈克尔·佩奇:操作系统实际上可以允许环 3 上的 IO 指令,这实际上是由任务状态段。
如果环 3 一开始没有获得许可,那么环 3 就不可能授予自己这样做的权限。
Linux 总是不允许它。也可以看看:https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss
程序和操作系统如何在环之间转换?
当CPU打开时,它开始运行环0中的初始程序(很好,但这是一个很好的近似值)。你可以认为这个初始程序是内核(但通常它是然后调用仍在环 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 会调用内核在启动时注册的中断回调处理程序。这里有一个注册处理程序并使用它的具体裸机示例。
该处理程序在环 0 中运行,它决定内核是否允许此操作、执行该操作并在环 3 中重新启动用户态程序。x86_64
当。。。的时候
exec
使用系统调用时(或者当内核将开始/init
), 内核准备寄存器和内存新的用户态进程,然后跳转到入口点并将CPU切换到环3如果程序尝试做一些顽皮的事情,例如写入禁止的寄存器或内存地址(由于分页),CPU 还会调用环 0 中的某些内核回调处理程序。
但由于用户态很顽皮,内核这次可能会杀死该进程,或者用信号给它一个警告。
当内核启动时,它会设置一个具有固定频率的硬件时钟,该时钟会定期生成中断。
该硬件时钟生成运行环 0 的中断,并允许它调度要唤醒的用户态进程。
这样,即使进程没有进行任何系统调用,也可以进行调度。
拥有多个戒指有什么意义?
分离内核和用户态有两个主要优点:
- 制作程序会更容易,因为您更确定一个程序不会干扰另一个程序。例如,一个用户态进程不必担心由于分页而覆盖另一程序的内存,也不必担心将硬件置于另一进程的无效状态。
- 它更安全。例如,文件权限和内存分离可以防止黑客应用程序读取您的银行数据。当然,这假设您信任内核。
如何玩转它?
我创建了一个裸机设置,这应该是直接操作环的好方法:https://github.com/cirosantilli/x86-bare-metal-examples
不幸的是,我没有耐心制作用户态示例,但我确实进行了分页设置,因此用户态应该是可行的。我很想看到拉取请求。
或者,Linux 内核模块在环 0 中运行,因此您可以使用它们来尝试特权操作,例如读取控制寄存器:https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers-cr0-cr2-cr3-from-a-program-getting-segmenta/7419306#7419306
这里有一个方便的 QEMU + Buildroot 设置尝试一下而不杀死你的主机。
内核模块的缺点是其他 kthread 正在运行,可能会干扰您的实验。但理论上你可以用你的内核模块接管所有中断处理程序并拥有系统,这实际上是一个有趣的项目。
负环
虽然英特尔手册中实际上并未引用负环,但实际上有些 CPU 模式比环 0 本身具有更多功能,因此非常适合“负环”名称。
一个例子是虚拟化中使用的管理程序模式。
欲了解更多详情,请参阅:
- 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 术语中的“管理程序”)。
使用指令(SuperVisor Call)输入
svc
,以前称为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 调用。
-
使用指令(HyperVisor 调用)输入
hvc
。虚拟机管理程序对于操作系统来说,就像操作系统对于用户空间一样。
例如,Xen 允许您同时在同一系统上运行多个操作系统,例如 Linux 或 Windows,并且它将操作系统彼此隔离以确保安全性和易于调试,就像 Linux 对用户态程序所做的那样。
虚拟机管理程序是当今云基础设施的关键部分:它们允许多个服务器在单个硬件上运行,使硬件使用率始终接近 100%,并节省大量资金。
例如,AWS 在 2017 年之前一直使用 Xen迁移到 KVM 成为新闻。
EL3:另一个级别。待办事项示例。
使用指令输入
smc
(安全模式呼叫)
这ARMv8 架构参考模型 DDI 0487C.a- 第 D1 章 - AArch64 系统级程序员模型 - 图 D1-1 精美地说明了这一点:
随着 ARM 的出现,情况发生了一些变化ARMv8.1 虚拟化主机扩展 (VHE)。这个扩展允许内核在 EL2 中高效运行:
VHE 的创建是因为 Linux 内核虚拟化解决方案(例如 KVM)已经超过了 Xen(参见上面提到的 AWS 转向 KVM),因为大多数客户端只需要 Linux VM,并且正如您可以想象的那样,所有这些都在一个单一的虚拟机中项目中,KVM 比 Xen 更简单并且可能更高效。因此,现在主机 Linux 内核在这些情况下充当虚拟机管理程序。
请注意,也许是出于事后诸葛亮的考虑,ARM 对权限级别的命名约定比 x86 更好,而不需要负级别:0 表示较低,3 表示最高。较高级别往往比较低级别更容易创建。
可以通过指令查询当前的EL MRS
:https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
ARM 不要求所有异常级别都存在,以允许不需要该功能来节省芯片面积的实现。 ARMv8“异常级别”说:
实现可能不包括所有异常级别。所有实现都必须包括 EL0 和 EL1。 EL2和EL3是可选的。
例如,QEMU 默认为 EL1,但可以使用命令行选项启用 EL2 和 EL3:https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulate-a53-power-up
代码片段在 Ubuntu 18.10 上测试。
答案3
如果是第一个,那么是否意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?
是的,在普通的 Linux 系统上就是这种情况。有一组“4G/4G”补丁在某个点上浮动,使用户和内核地址空间完全独立(以性能为代价,因为它使内核更难访问用户内存),但我不认为它们曾被上游合并,随着 x86-64 的兴起,人们的兴趣逐渐减弱
另外,在这种情况下,内核如何使用高内存,因为高内存中的页面将映射到哪个虚拟内存地址,因为 1GB 的内核空间将被逻辑映射?
Linux过去的工作方式(并且在内存与地址空间相比较小的系统上仍然如此)是整个物理内存被永久映射到地址空间的内核部分。这允许内核无需重新映射即可访问所有物理内存,但显然它无法扩展到具有大量物理内存的 32 位计算机。
于是低内存和高内存的概念就诞生了。 “低”内存被永久映射到内核地址空间。 “高”记忆则不然。
当处理器运行系统调用时,它在内核模式下运行,但仍在当前进程的上下文中。因此它可以直接访问当前进程的内核地址空间和用户地址空间(假设您没有使用前面提到的4G/4G补丁)。这意味着将“高”内存分配给用户态进程是没有问题的。
将“高”内存用于内核目的更是一个问题。要访问未映射到当前进程的高端内存,必须将其临时映射到内核的地址空间。这意味着额外的代码和性能损失。