Linux内存分段

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (133)

看一下Linux和内存管理的内部结构,我偶然发现了Linux使用的分段分页模型。

如果我错了,请纠正我,但Linux(保护模式)确实使用分页将线性虚拟地址空间映射到物理地址空间。由页面构成的线性地址空间被分成用于处理平面存储器模型的四个段,即:

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

存在称为空段的第五存储器段但未使用。

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

为了简单起见,我将专注于32位内存映射,具有4GiB可地址空间,3GiB用于用户空间进程空间(以绿色显示),1GiB用于管理程序内核空间(以红色显示):

所以,红色部分由两个部分组成__KERNEL_CS,并__KERNEL_DS和两个段的绿色部分__USER_CS__USER_DS

这些部分彼此重叠。分页将用于userland和内核隔离。

然而,随着维基百科中提取这里

[...]许多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),

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

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

此外,由于所有段彼此重叠,是否提供存储器保护(即存储器分离)的寻呼单元?

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

提问于
用户回答回答于

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

由页面构成的该线性地址空间被分成四个段

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

所以,红色部分由两个部分组成__KERNEL_CS,并__KERNEL_DS

不,这是你出错的地方。 x86段寄存器用于分段; 它们是x86传统行李,仅用于x86-64上的CPU模式和权限级别选择。AMD没有为长模式添加新的机制并完全丢弃段,而只是在长模式下进行了绝对分段(基本固定为0,就像所有在32位模式中使用的所有人一样)并且仅将段用于机器配置目的而不是除非你实际编写的代码切换到32位模式或其他任何东西,否则特别有趣。

(除非您可以为FS和/或GS设置非零基础,Linux也会为线程本地存储设置。但这与copy_from_user()实现方式或任何内容无关。它只需要检查指针值,不参考段描述符的任何段或CPL / RPL。)

在32位传统模式中,可以编写使用分段内存模型的内核,但是没有一个主流操作系统实际上这样做。有些人希望这已成为一件事,但是,例如看到这个答案感叹x86-64使得Multics风格的操作系统变得不可能。但这不是 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)。

但那些旗帜是什么0xc09b0xa09b等等?我倾向于认为他们是细分选择者

不,分段选择器是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_CSUSER32_CS。32位代码段描述符的L位将被清除。当前的CS段描述是将x86-64 CPU置于32位compat模式与64位长模式之间的原因。要输入的32位用户空间,一个iretsysret将设置CS:RIP到用户模式32位的段选择。

认为你也可以让CPU处于16位compat模式(比如compat模式不是实模式,但默认的操作数大小和地址大小是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位代码段清除。显然,类型(代码与数据)和权限级别不同。

用户回答回答于

我发布这个答案是为了清除任何误解的话题(正如@PeterCordes所指出的那样)。

分页

在Linux中(x86保护模式)的存储器管理使用分页的物理地址映射到一个完全虚拟化的平坦的线性寻址空间,从0x000000000xFFFFFFFF(32位),被称为平面内存模型。Linux与CPU的MMU(内存管理单元)一起将每个虚拟地址1:1映射到相应的物理地址。物理内存通常分为4KiB页面,以便更容易地管理内存。

一般而言,物理地址不能直接解除引用。启用分页后,I / O端口和总线地址也会以1:1的比例映射到虚拟地址,并且必须调用ioremap()以手动将物理地址关联到可以取消引用的虚拟地址。这并非总是如此,因为内存地址以1:1的比例映射到逻辑地址(使用__pa()__va()宏),但这超出了本主题的范围。

更详细地,4KiB的物理存储器页面被映射到OS页面表中的虚拟地址,每个映射称为PTE(页表条目)。然后,CPU的MMU将从OS页表中保留每个最近使用的PTE的缓存。这个缓存区域称为TLB(Translation Lookaside Buffer)。该cr3寄存器用于定位OS页表。

每当需要将虚拟地址转换为物理地址时,将搜索TLB。如果找到匹配(TLB 命中),则返回并访问物理地址。但是,如果没有匹配(TLB未命中),则TLB未命中处理程序将查找页表以查看是否存在映射(页面遍历)。如果存在,则将其写回TLB并重新启动故障指令,此后续转换将发现TLB 命中并且存储器访问将继续。这称为次要页面错误。

有时,操作系统可能需要通过将页面移动到硬盘来增加物理RAM的大小。如果虚拟地址解析为硬盘中映射的页面,则需要在访问之前将页面加载到物理RAM中。这被称为主要页面错误。然后页面错误处理程序需要在内存中找到一个空闲页面。

如果没有可用于虚拟地址的映射,则转换过程可能失败,这意味着虚拟地址无效。这就是所谓的无效页面错误例外,和段错误会发出进程。

内存分段

真实模式

实模式仍然使用20位分段存储器地址空间,具有1MiB的可寻址存储器(0x00000- 0xFFFF0),并且可以无限制地直接访问所有可寻址存储器,I / O端口和总线地址以及外围硬件。实模式不提供内存保护,没有权限级别,也没有虚拟化地址。通常,段寄存器包含段选择器值,而存储器操作数是相对于段基的偏移值。

为了解决分段(C编译器通常只支持平面内存模型),C编译器使用非官方指针类型来表示具有segment:offset逻辑地址表示法的物理地址。例如,逻辑地址0x5555:0x0005在计算后0x5555 * 16 + 0x0005产生20位物理地址0x55555,可在远指针中使用,如下所示:

char far    *ptr;           /* declare a far pointer */
ptr = (char far *)0x55555;  /* initialize a far pointer */

截至今天,大多数现代x86 CPU仍然以实模式启动以实现向后兼容,之后切换到保护模式。

保护模式

在保护模式下,使用平坦内存模型时未使用分段。这四个部分,分别是__KERNEL_CS__KERNEL_DS__USER_CS__USER_DS都有着各自的基地址设置为0。这些片段是从那里用分段存储器管理前者的x86模式只是传统的行李。在保护模式下,由于所有段基址都设置为0,因此逻辑地址等效于线性地址。

具有平坦内存模型的保护模式意味着没有分段。

然而,段寄存器如ss(堆栈段寄存器), ds(数据段寄存器)或cs(代码段寄存器)仍然存在,并且用于存储16位段选择器,其包含索引段的描述符中的LDT和GDT(本地及全局描述符表)。

触发存储器的每条指令都隐含地使用段寄存器。根据上下文,使用特定的段寄存器。例如,JMP指令在使用csPUSH使用ss。可以使用指令将选择器加载到寄存器中MOV,唯一的例外是cs寄存器,该寄存器仅由影响执行流程的指令修改,如CALLJMP

cs寄存器特别有用,因为它在其段选择器中跟踪CPL(当前特权级别),从而保留当前段的特权级别。此2位CPL值始终等于CPU当前权限级别。

记忆保护

分页

CPU权限级别(也称为模式位或保护环)从0到3,限制某些指令可能会破坏保护机制或在用户模式下允许引起混乱,因此它们保留给内核。尝试在ring 0之外运行它们会导致一般保护故障异常,当发生无效段访问错误(特权,类型,限制,读/写权限)时也是如此。同样,基于权限级别限制对内存和I / O端口的任何访问,并且每次尝试访问没有所需权限级别的受保护页面都将导致页面错误异常。

只要发生中断请求(IRQ)(软件(即系统调用)或硬件),模式位就会自动从用户模式切换到超级用户模式。

Linux(启用分页)使用称为高半内核的保护模式,其中扁平寻址空间被划分为两个虚拟地址范围。范围0xC0000000- 0xFFFFFFFF被保留为内核数据和代码,这是主管空间(红色区域),仅在环0。这接近离开的范围0x00000000- 0xBFFFFFFF用户态空间(绿色区域),其中用户代码,数据和库驻留,可从环0和环3访问。

边界地址之间的主管半部和用户态下的部分被称为TASK_SIZE_MAX在Linux内核中。内核将检查来自任何用户空间进程的每个访问的虚拟地址是否位于该边界之下,如下面的代码所示:

static int fault_in_kernel_space(unsigned long address)
{
    /*
     * On 64-bit systems, the vsyscall page is at an address above
     * TASK_SIZE_MAX, but is not considered part of the kernel
     * address space.
     */
    if (IS_ENABLED(CONFIG_X86_64) && is_vsyscall_vaddr(address))
        return false;

    return address >= TASK_SIZE_MAX;
}

如果userland进程尝试访问高于的内存地址TASK_SIZE_MAX,则do_kern_addr_fault() 例程将调用__bad_area_nosemaphore() 例程,最终通过a SIGSEGVget_current()用于获取task_struct)发出错误信号:

/*
 * To avoid leaking information about the kernel page table
 * layout, pretend that user-mode accesses to kernel addresses
 * are always protection faults.
 */
if (address >= TASK_SIZE_MAX)
    error_code |= X86_PF_PROT;

force_sig_fault(SIGSEGV, si_code, (void __user *)address, tsk); /* Kill the process */

分割

使用分段的较旧架构通常使用每个请求段的GDT特权位执行段访问验证。将所请求的段的特权位(称为DPL(描述符特权级别))与当前段的CPL进行比较,确保CPL <= DPL。如果为true,则允许内存访问到请求的段。

扫码关注云+社区

领取腾讯云代金券