Linux内存分段

时间:2019-05-20 02:04:58

标签: linux linux-kernel x86 osdev memory-segmentation

在研究Linux和内存管理的内部结构时,我偶然发现了Linux使用的分段分页模型。

如果我错了,请纠正我,但是Linux(保护模式)确实使用分页将线性虚拟地址空间映射到物理地址空间。由页面组成的线性地址空间,对于过程平面内存模型分为四个部分,即:

  • 内核代码段(__KERNEL_CS);
  • 内核数据段(__KERNEL_DS);
  • 用户代码段(__USER_CS);
  • 用户数据段(__USER_DS);

存在第五个内存段,称为Null段,但未使用。

这些段的CPL(当前特权级别)为0(主管)或3(用户权限)。

为简单起见,我将集中讨论32位内存映射,其中4GiB可寻址空间,3GiB用于用户空间进程空间(以绿色显示),1GiB用于主管内核空间(以红色显示)。 :

Virtual Memory Space

因此,红色部分由两个分段__KERNEL_CS__KERNEL_DS组成,绿色部分由两个分段__USER_CS__USER_DS组成。 < / p>

这些段彼此重叠。分页将用于用户区和内核隔离。

但是,摘自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开始,但是那些标志分别是0xc09b0xa09b等?我倾向于认为它们是段选择器,如果不是,那么如果它们的寻址空间都从0开始,我将如何从内核段访问用户区段?

不使用分段。仅使用分页。段的seg_base地址设置为0,将其空间扩展到0xFFFFF,从而提供完整的线性地址空间。这意味着逻辑地址与线性地址没有什么不同。

此外,由于所有段彼此重叠,是提供内存保护(即内存分离)的分页单元吗?

分页提供保护,而不是分段。内核将检查线性地址空间,并根据边界(通常称为TASK_MAX)检查安全性级别。请求的页面。

1 个答案:

答案 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)。


  

但是这些标志是什么,即0xc09b0xa09b等?我倾向于相信它们是细分选择器

否,分段选择器是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以查找内存中的匹配数据!

GDT数据格式:

您正在查看x86-64 Linux源代码,因此内核将处于long模式,而非保护模式。我们可以知道,因为USER_CSUSER32_CS有单独的条目。 32位代码段描述符将清除其L位。当前的CS段描述是什么使x86-64 CPU进入32位兼容模式和64位长模式。要输入32位用户空间,iretsysret会将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位代码段。显然,类型(代码与数据)和特权级别有所不同。