在研究Linux和内存管理的内部结构时,我偶然发现了Linux使用的分段分页模型。
如果我错了,请纠正我,但是Linux(保护模式)确实使用分页将线性虚拟地址空间映射到物理地址空间。由页面组成的线性地址空间,对于过程平面内存模型分为四个部分,即:
__KERNEL_CS
); __KERNEL_DS
); __USER_CS
); __USER_DS
); 存在第五个内存段,称为Null段,但未使用。
这些段的CPL(当前特权级别)为0(主管)或3(用户权限)。
为简单起见,我将集中讨论32位内存映射,其中4GiB可寻址空间,3GiB用于用户空间进程空间(以绿色显示),1GiB用于主管内核空间(以红色显示)。 :
因此,红色部分由两个分段 < / p>
__KERNEL_CS
和__KERNEL_DS
组成,绿色部分由两个分段__USER_CS
和__USER_DS
组成。
这些段彼此重叠。分页将用于用户区和内核隔离。
但是,摘自Wikipedia here:
[...]许多32位操作系统通过将所有段的基数都设置为0来模拟平面存储器模型,以使分段对程序无关。
查看GDT here的linux内核代码:
[GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
正如Peter指出的那样,每个段都从0开始,但是那些标志分别是 0xc09b
,0xa09b
等?我倾向于认为它们是段选择器,如果不是,那么如果它们的寻址空间都从0开始,我将如何从内核段访问用户区段?
不使用分段。仅使用分页。段的seg_base
地址设置为0,将其空间扩展到0xFFFFF
,从而提供完整的线性地址空间。这意味着逻辑地址与线性地址没有什么不同。
此外,由于所有段彼此重叠,是提供内存保护(即内存分离)的分页单元吗?
分页提供保护,而不是分段。内核将检查线性地址空间,并根据边界(通常称为TASK_MAX
)检查安全性级别。请求的页面。
答案 0 :(得分:7)
是的,Linux使用分页,因此所有地址始终都是虚拟的。 (要访问位于已知物理地址的内存,Linux会将所有物理内存1:1映射到一定范围的内核虚拟地址空间,因此它可以简单地使用物理地址作为偏移量索引到该“数组”中。复杂度为32具有比内核地址空间更多的物理RAM的系统上的位内核。)
由页面组成的线性地址空间分为四个部分
否,Linux使用平面内存模型。所有这四个段描述符的基数和限制均为0和-1(无限制)。即它们全部完全重叠,覆盖了整个32位虚拟线性地址空间。
因此红色部分包括两个部分
__KERNEL_CS
和__KERNEL_DS
否,这是您出错的地方。 x86段寄存器不用于分段;它们是x86的传统行李,仅用于x86-64上的CPU模式和特权级别选择。 AMD并没有为此添加新的机制并完全删除长模式的分段,而只是在长模式下绝杀分段(就像固定在32位模式下的所有人一样,基本固定为0),并且仅将分段用于机器配置目的,而并非除非您实际上正在编写切换到32位模式的代码,否则它特别有趣。
(除了可以为FS和/或GS设置非零基数,而Linux可以为线程本地存储设置非零基数。但这与copy_from_user()
的实现方式无关,或其他任何方面。只需检查该指针值,而不必参考任何段或段描述符的CPL / RPL。)
在32位传统模式下,可以编写使用分段内存模型的内核,但是主流操作系统都没有这样做。不过,有些人希望这已成为一件事情。参见this answer lamenting x86-64 making a Multics-style OS impossible。但这不是Linux的工作方式。
Linux是https://wiki.osdev.org/Higher_Half_Kernel,其中内核指针具有一个值范围(红色部分),而用户空间地址位于绿色部分。如果映射了正确的用户空间页表,内核可以简单地取消对用户空间地址的引用,它不需要转换它们或对段进行任何操作。 这就是拥有平面内存模型的意思。 (内核可以使用“用户”页表条目,但不能,反之亦然)。对于x86-64,请参阅https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt了解实际的内存映射。
这4个GDT条目都需要分开的唯一原因是出于特权级的原因,并且数据段与代码段的描述符具有不同的格式。 (GDT条目不仅包含基本/限制;这些是需要不同的部分。请参见https://wiki.osdev.org/Global_Descriptor_Table)
尤其是https://wiki.osdev.org/Segmentation#Notes_Regarding_C,它描述了“普通”操作系统通常如何以及为何使用GDT来创建平面内存模型,并为每个特权级别提供了一对代码和数据描述符。
对于32位Linux内核,只有gs
获得线程本地存储的非零基数(因此,像[gs: 0x10]
这样的寻址方式将访问依赖于执行线程的线性地址它)。或在64位内核(和64位用户空间)中,Linux使用fs
。 (因为x86-64通过swapgs
指令使GS变得特别,该指令旨在与syscall
一起用于内核以查找内核堆栈。)
但是无论如何,FS或GS的非零基不是来自GDT条目,而是使用wrgsbase
指令设置的。 (或者在不支持该功能的CPU上写入MSR)。
但是这些标志是什么,即
0xc09b
,0xa09b
等?我倾向于相信它们是细分选择器
否,分段选择器是GDT的索引。内核使用[GDT_ENTRY_KERNEL32_CS] = initializer_for_that_selector
之类的指定初始化语法将GDT定义为C数组。
(实际上,选择器的低2位(即段寄存器值)是当前特权级别。因此GDT_ENTRY_DEFAULT_USER_CS
应该是`__USER_CS >>2。)
mov ds, eax
触发硬件为GDT编制索引,而不是线性搜索GDT以查找内存中的匹配数据!
您正在查看x86-64 Linux源代码,因此内核将处于long模式,而非保护模式。我们可以知道,因为USER_CS
和USER32_CS
有单独的条目。 32位代码段描述符将清除其L
位。当前的CS段描述是什么使x86-64 CPU进入32位兼容模式和64位长模式。要输入32位用户空间,iret
或sysret
会将CS:RIP设置为用户模式的32位段选择器。
我认为您也可以使CPU处于16位兼容模式(例如,兼容模式不是实模式,但默认操作数大小和地址大小为16)。但是Linux不会这样做。
无论如何,如https://wiki.osdev.org/Global_Descriptor_Table和细分中所述,
每个段描述符都包含以下信息:
- 段的基地址
- 段中的默认操作大小(16位/ 32位)
- 描述符的特权级别(Ring 0-> Ring 3)
- 粒度(段限制以字节/ 4kb为单位)
- 细分限制(细分中的最大合法偏移量)
- 段存在(是否存在)
- 描述符类型(0 =系统; 1 =代码/数据)
- 段类型(代码/数据/读取/写入/访问/符合/不符合/扩展/扩展/向下扩展)
这些是多余的位。我对哪个位不特别感兴趣,因为我(认为我)了解了不同GDT条目的用途和作用的高级知识,而无需深入了解其实际编码方式。
但是,如果您查看x86手册或osdev Wiki,以及这些初始化宏的定义,则应该发现它们导致GDT条目的L
位设置为64位代码段,已清除适用于32位代码段。显然,类型(代码与数据)和特权级别有所不同。