Answers:
当代表用户程序(即系统调用)执行内核时,是否使用了内核空间?还是所有内核线程(例如调度程序)的地址空间?
是的,是的。
在继续之前,我们应该声明有关内存的信息。
内存获取分为两个不同的区域:
在用户空间下运行的进程只能访问内存的有限部分,而内核可以访问所有内存。在用户空间中运行的进程也无权访问内核空间。用户空间进程只能通过内核公开的接口(系统调用)访问内核的一小部分。如果进程执行系统调用,则会将软件中断发送到内核,然后内核将分派适当的中断处理程序,并在处理程序完成后继续其工作。
内核空间代码具有在“内核模式”下运行的属性,(在典型的台式机-x86-计算机上)是您在环0下执行的代码。通常在x86架构中,有4个保护环。环0(内核模式),环1(可由虚拟机管理程序或驱动程序使用),环2(可由驱动程序使用,但是我不太确定)。环3是典型应用程序的运行环境。它是特权最小的环,并且在其上运行的应用程序可以访问处理器指令的子集。环0(内核空间)是特权最高的环,可以访问所有机器指令。例如,“普通”应用程序(如浏览器)不能使用x86汇编指令lgdt
加载全局描述符表或hlt
暂停处理器。
如果是第一个,则意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?另外,在那种情况下内核如何使用高级内存,因为逻辑上将映射1GB内核空间,因此高级内存中的页面将映射到哪个虚拟内存地址?
-1
适用于管理程序?en.wikipedia.org/wiki/Protection_ring
CPU环是最明显的区别
在x86保护模式下,CPU始终处于4个振铃之一。Linux内核仅使用0和3:
这是内核vs用户态的最难,最快速的定义。
为什么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无法运行多个指令并无法写入多个寄存器,最值得注意的是:
不能改变自己的戒指!否则,它可能会将自己设置为环0,并且环将无用。
换句话说,不能修改确定当前环的当前段描述符。
无法修改页表:https : //stackoverflow.com/questions/18431261/how-does-x86-paging-work
换句话说,不能修改CR3寄存器,分页本身会阻止页表的修改。
出于安全性/易于编程的原因,这可防止一个进程看到其他进程的内存。
无法注册中断处理程序。通过写入存储位置来配置这些地址,这也可以通过分页来防止。
处理程序在环0中运行,并且会破坏安全模型。
换句话说,不能使用LGDT和LIDT指令。
无法执行IO指令(如in
和)out
,因此可以进行任意硬件访问。
否则,例如,如果任何程序可以直接从磁盘读取,则文件权限将无用。
更确切地说,要感谢Michael Petch:操作系统实际上可能在环3上允许IO指令,这实际上是由Task状态段控制的。
如果环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 syscall你好世界示例:
.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中重新启动userland程序。x86_64
当使用exec
系统调用时(或内核将启动时/init
),内核准备新的userland进程的寄存器和内存,然后跳转到入口点并将CPU切换到ring 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- / 7419306#7419306
这是一个方便的QEMU + Buildroot设置,可以在不杀死主机的情况下进行尝试。
内核模块的缺点是其他kthreads正在运行,并且可能会干扰您的实验。但是从理论上讲,您可以使用内核模块来接管所有的中断处理程序并拥有系统,这实际上是一个有趣的项目。
负环
尽管英特尔手册中并未实际提及负环,但实际上有一些CPU模式具有比环0本身更多的功能,因此非常适合“负环”名称。
一个示例是虚拟化中使用的管理程序模式。
有关更多详细信息,请参见:https : //security.stackexchange.com/questions/129098/what-is-protection-ring-1
臂
在ARM中,环被称为“异常级别”,但主要思想保持不变。
ARMv8中存在4个异常级别,通常用作:
EL0:用户区
EL1:内核(ARM术语中的“主管”)。
与svc
指令(SuperVisor调用)一起输入,该指令以前称为swi
统一汇编之前,它是用于进行Linux系统调用的指令。Hello world ARMv8示例:
.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调用)。
虚拟机管理程序是针对操作系统的,就像操作系统是针对用户的。
例如,Xen允许您在同一系统上同时运行多个操作系统(例如Linux或Windows),并且为了安全性和调试的方便性,它们相互隔离,就像Linux的用户界面程序一样。
虚拟机管理程序是当今云基础架构的关键部分:它们允许多个服务器在单个硬件上运行,从而使硬件使用率始终接近100%,并节省了大量资金。
例如,直到2017年AWS 迁移到KVM时才使用Xen 。
EL3:再上一层。TODO示例。
输入smc
说明(安全模式调用)
在ARMv8架构参考模型DDI 0487C.a - D1章-的AArch64系统级编程模型-图D1-1说明了这美丽的:
请注意,也许是由于事后观察的好处,ARM如何比x86具有更好的特权级别命名约定,而又不需要否定级别:0为最低,3为最高。较高的级别往往比较低的级别创建的频率更高。
可以使用以下MRS
指令查询当前的EL :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-emulation-a53-power-up
代码片段已在Ubuntu 18.10上进行了测试。
如果是第一个,则意味着普通用户程序不能拥有超过3GB的内存(如果划分为3GB + 1GB)?
是的,在普通的linux系统上就是这种情况。有一组“ 4G / 4G”补丁在某个点上浮动,使用户和内核地址空间完全独立(以性能为代价,因为这使内核更难访问用户内存),但我认为它们曾经被合并到上游,并且随着x86-64的兴起,兴趣减弱了
另外,在那种情况下内核如何使用高级内存,因为逻辑上将映射1GB内核空间,因此高级内存中的页面将映射到哪个虚拟内存地址?
linux过去的工作方式(在与地址空间相比内存较小的系统上仍然有效)是将整个物理内存永久映射到地址空间的内核部分。这使内核无需重新映射即可访问所有物理内存,但显然它无法扩展到具有大量物理内存的32位计算机。
因此,低内存和高内存的概念诞生了。“低”内存被永久映射到内核地址空间。“高”内存不是。
当处理器运行系统调用时,它以内核模式运行,但仍在当前进程的上下文中。因此,它可以直接访问当前进程的内核地址空间和用户地址空间(假设您未使用上述4G / 4G补丁程序)。这意味着将“高”内存分配给用户态进程没有问题。
使用“高”内存用于内核是一个更大的问题。要访问未映射到当前进程的高内存,必须将其暂时映射到内核的地址空间。这意味着额外的代码和性能损失。