gat*_*sec 6 linux x86 osdev linux-kernel memory-segmentation
在研究Linux内部和内存管理时,我偶然发现Linux使用的分段分页模型。
如果我错了,请纠正我,但是Linux(保护模式)确实使用分页将线性虚拟地址空间映射到物理地址空间。由页面组成的线性地址空间,对于进程平面内存模型分为四个部分,即:
__KERNEL_CS
);__KERNEL_DS
);__USER_CS
);__USER_DS
);存在第五个内存段,称为Null段,但未使用。
这些段的CPL(当前特权级别)为0(主管)或3(用户区域)。
为简单起见,我将集中讨论32位内存映射,其中4GiB可寻址空间,3GiB用于用户空间进程空间(以绿色显示),1GiB用于主管内核空间(以红色显示):
因此红色的部分由两个部分__KERNEL_CS
和组成,__KERNEL_DS
绿色的部分由两个部分__USER_CS
和组成__USER_DS
。
这些段彼此重叠。分页将用于用户空间和内核隔离。
但是,从Wikipedia 此处提取:
daccess-ods.un.org daccess-ods.un.org许多32位操作系统都通过将所有段的基数都设置为0来模拟平面存储器模型,以使分段对程序无关。
在这里查看GDT的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),
Run Code Online (Sandbox Code Playgroud)
正如Peter所指出的,每个段都从0开始,但是那些标志分别是0xc09b
,0xa09b
等等?我倾向于认为它们是段选择器,如果不是,那么如果它们的寻址空间都从0开始,我将如何从内核段访问userland段?
不使用细分。仅使用分页。段的seg_base
地址设置为0,将其空间扩展到0xFFFFF
,从而提供完整的线性地址空间。这意味着逻辑地址与线性地址没有区别。
另外,由于所有段彼此重叠,提供内存保护(即内存分离)的是分页单元吗?
分页提供保护,而不是分段。内核将检查线性地址空间,并根据边界(通常称为TASK_MAX
)检查请求页面的特权级别。
是的,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位传统模式下,可以编写使用分段内存模型的内核,但是主流操作系统都没有这样做。但是,有些人希望这已经成为一件事情,例如,看到这个回答感叹x86-64使Multics风格的OS变得不可能。但这不是 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使GS与swapgs
指令相对应,该指令旨在与syscall
内核一起使用以查找内核堆栈。)
但是无论如何,FS或GS的非零基不是来自GDT条目,而是通过wrgsbase
指令设置的。(或者在不支持该功能的CPU上写入MSR)。
但是这些标志是什么
0xc09b
,0xa09b
等等?我倾向于认为它们是细分选择器
不,细分选择器是GDT的索引。内核使用指定的初始化程序语法将GDT定义为C数组[GDT_ENTRY_KERNEL32_CS] = initializer_for_that_selector
。
(实际上,选择器的低2位(即段寄存器值)是当前特权级别。因此,GDT_ENTRY_DEFAULT_USER_CS
应该是`__USER_CS >>2。)
mov ds, eax
触发硬件为GDT编制索引,而不是线性搜索GDT以查找内存中的匹配数据!
您正在查看x86-64 Linux源代码,因此内核将处于长模式而不是受保护模式。我们可以说,因为有单独的入口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和Segmentation中所述,
每个段描述符包含以下信息:
- 段的基地址
- 段中的默认操作大小(16位/ 32位)
- 描述符的特权级别(Ring 0-> Ring 3)
- 粒度(段限制以字节/ 4kb为单位)
- 细分限制(细分中的最大合法偏移量)
- 段存在(是否存在)
- 描述符类型(0 =系统; 1 =代码/数据)
- 段类型(代码/数据/读取/写入/访问/符合/不符合/扩大/扩大/缩小)
这些是额外的位。我对哪个位不特别感兴趣,因为我(认为我)了解了不同GDT条目的用途和作用的高级知识,而没有深入了解其实际编码方式。
但是,如果您查看x86手册或osdev Wiki,以及这些init宏的定义,则应该发现它们导致GDT条目的L
位设置为64位代码段,而对32位代码段清除。显然,类型(代码与数据)和特权级别有所不同。