gpt4 book ai didi

Linux内存分段

转载 作者:IT王子 更新时间:2023-10-29 00:47:41 25 4
gpt4 key购买 nike

在研究 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 .

    这些部分相互重叠。 分页将用于用户态和内核隔离。

    然而,正如摘自维基百科 here :

    [...] many 32-bit operating systems simulate a flat memory model by setting all segments' bases to 0 in order to make segmentation neutral to programs.



    查看 GDT 的 linux 内核代码 here :
    [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),

    正如彼得指出的那样,每个段都从 0 开始,但是那些标志是什么,即 0xc09b , 0xa09b等等?我倾向于相信它们是段选择器,如果不是,如果它们的寻址空间都从 0 开始,我将如何从内核段访问用户空间段?

    不使用分段。 只使用分页。段有他们的 seg_base地址设置为 0,将它们的空间扩展到 0xFFFFF从而给出完整的线性地址空间。这意味着逻辑地址与线性地址没有区别。

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

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

    最佳答案

    是的,Linux 使用分页,因此所有地址始终是虚拟的。 (为了访问已知物理地址的内存,Linux 将所有物理内存 1:1 映射到内核虚拟地址空间的范围,因此它可以使用物理地址作为偏移量简单地索引到该“数组”。32 的模复杂度-bit 内核在物理 RAM 多于内核地址空间的系统上。)

    This linear address space constituted of pages, is split into four segments



    不,Linux 使用平面内存模型。所有 4 个段描述符的基数和限制都是 0 和 -1(无限制)。即 它们都完全重叠,覆盖了整个 32 位虚拟线性地址空间。

    So the red part consists of two segments __KERNEL_CS and __KERNEL_DS



    不,这是你出错的地方。 x86 段寄存器不用于分段;它们是 x86 遗留包,仅用于 x86-64 上的 CPU 模式和特权级别选择 . AMD 并没有为此添加新机制并完全为长模式删除段,而是在长模式下中性分段(基数固定为 0,就像在 32 位模式下使用的每个人一样),并仅将段用于机器配置目的,而不是特别有趣,除非您实际上正在编写切换到 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)。

    but what are those flags, namely 0xc09b, 0xa09b and so on ? I tend to believe they are the segments selectors



    不,段选择器是 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 位用户空间,一个 iretsysret将 CS:RIP 设置为用户模式 ​​32 位段选择器。

    我认为你也可以让 CPU 处于 16 位兼容模式(就像兼容模式不是实模式,但默认的操作数大小和地址大小是 16)。但是,Linux 不会这样做。

    无论如何,如 https://wiki.osdev.org/Global_Descriptor_Table 中所述和分割,

    Each segment descriptor contains the following information:

    • The base address of the segment
    • The default operation size in the segment (16-bit/32-bit)
    • The privilege level of the descriptor (Ring 0 -> Ring 3)
    • The granularity (Segment limit is in byte/4kb units)
    • The segment limit (The maximum legal offset within the segment)
    • The segment presence (Is it present or not)
    • The descriptor type (0 = system; 1 = code/data)
    • The segment type (Code/Data/Read/Write/Accessed/Conforming/Non-Conforming/Expand-Up/Expand-Down)


    这些是额外的位。我对哪些位不是特别感兴趣,因为我(认为我)了解不同 GDT 条目的用途及其作用的高级图片,而没有深入了解其实际编码方式的细节。

    但是,如果您查看 x86 手册或 osdev wiki 以及这些 init 宏的定义,您应该会发现它们会生成带有 L 的 GDT 条目。为 64 位代码段设置位,为 32 位代码段清除位。显然类型(代码与数据)和权限级别不同。

    关于Linux内存分段,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56213569/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com