Linux是否不使用分段而是仅使用分页?


24

Linux编程接口显示了进程的虚拟地址空间的布局。图中的每个区域都是一个分段吗?

在此处输入图片说明

通过了解Linux内核

下列意思是否正确,即MMU中的分段单元将分段和分段内的偏移量映射到虚拟内存地址,然后分页单元将虚拟内存地址映射到物理内存地址?

存储器管理单元(MMU)通过称为分段单元的硬件电路将逻辑地址转换为线性地址。随后,称为分页单元的第二个硬件电路将线性地址转换为物理地址(见图2-1)。

在此处输入图片说明

那么为什么说Linux不使用分段而是仅使用分页呢?

分割已包含在80x86微处理器中,以鼓励程序员将其应用程序拆分为逻辑上相关的实体,例如子例程或全局和本地数据区域。但是, Linux以非常有限的方式使用分段。实际上,分段和分页在某种程度上是多余的,因为它们都可以用于分隔进程的物理地址空间:分段可以为每个进程分配不同的线性地址空间,而分页可以将同一线性地址空间映射到不同的物理地址空间。Linux倾向于分页而不是分段,原因如下:

•当所有进程使用相同的段寄存器值(即它们共享同一组线性地址)时,内存管理会更简单。

•Linux的设计目标之一是可移植到多种体系结构。特别是RISC体系结构,对分段的支持有限。

Linux 2.6版仅在80x86体系结构要求时才使用分段。


您能指定版本吗?指定作者姓名也可能会有所帮助。我至少知道第一个是来自杰出人物。但是,两个标题名称都有点通用,起初我不清楚您是在谈论书:-)。
sourcejedi

2
关于“细分已包含在80x86微处理器中……”:事实并非如此。它是808x处理器的遗留物,它具有16位数据指针和64 Kb内存段。段指针允许您切换段以寻址更多内存。该体系结构延续到80x86(指针大小增加到33位)。如今,在x86_64模型中,您拥有64位指针(理论上-我认为实际上仅使用48位)可以寻址16艾字节,因此段不是必需的。
jamesqf

2
@jamesqf,好吧,386中的32位保护模式所支持的段与它们在8086中的16字节可缩放指针完全不同,因此,这不仅仅是简单的传统。当然,这并不是说它们的用处。
ilkkachu

@jamesqf 80186具有与8086相同的内存模型,没有“ 33位”
Jasen

没有值得回答的问题,因此只提供一条评论:段和页面仅在交换(例如页面交换与段交换)的上下文中具有可比性,而在这种情况下,页面交换只是将段交换从水中吹了出来。如果您交换段的进/出,则需要交换整个段,可能是2-4GB。这从未在x86上使用过。使用页面,您可以始终以4KB为单位工作。当涉及到访问内存时,段和页通过页表相关联,并且比较是苹果与橘子。
vhu

Answers:


20

x86-64体系结构在长模式(64位模式)下不使用分段。

段寄存器中的四个:CS,SS,DS和ES被强制为0,限制为2 ^ 64。

https://zh.wikipedia.org/wiki/X86_memory_segmentation#Later_developments

OS不再可能限制“线性地址”的范围。因此,它不能将分段用于内存保护。它必须完全依靠分页。

不必担心x86 CPU的细节,这些细节仅在传统的32位模式下运行时才适用。Linux很少使用32位模式。它甚至可以被视为“处于良性忽视状态多年”。请参阅Fedora中的32位x86支持 [LWN.net,2017]。

(碰巧32位Linux也没有使用分段。但是您不需要对此信任我,您可以忽略它:-)。


这有点夸大其词。对于旧版原始8086分段(CS / DS / ES / SS),长模式下的base / limit固定为0 / -1,但是FS和GS仍具有任意分段基础。加载到CS中的段描述符确定CPU是在32位还是64位模式下执行。而且,x86-64 Linux上的用户空间使用FS进行线程本地存储(mov eax, [fs:rdi + 16])。内核使用GS(在之后swapgs)在syscall入口点中查找当前进程的内核堆栈。但是是的,分段不被用作主要操作系统内存管理/内存保护机制的一部分。
彼得·科德斯

这基本上是问题中的引号所说的意思:“ 2.6版本的Linux仅在80x86体系结构要求时才使用分段。” 但是您的第二段基本上是错误的。Linux在32位和64位模式下使用分段基本上相同。即对于常规指令隐式使用的经典段(CS / DS / ES / SS),base = 0 / limit = 2 ^ 32或2 ^ 64。在32位Linux代码中没有什么多余的担心。硬件功能在那里但没有使用。
彼得·科德斯

@PeterCordes您基本上是在将答案解释为错误:-)。因此,我对其进行了编辑,以使我的论点变得不那么模糊。
sourcejedi 18/09/17

很好的改进,现在没有误导。我完全同意您的真实观点,那就是您可以并且应该完全忽略x86分段,因为它仅用于osdev系统管理和TLS。如果您想最终了解它,那么在您已经了解了带有平面内存模型的x86 asm之后,了解起来会容易得多。
彼得·科德斯

8

由于x86具有段,因此无法不使用它们。但是cs(代码段)和ds(数据段)基地址都设置为零,因此实际上并没有使用分段。线程本地数据是一个例外,通常未使用的段寄存器之一指向线程本地数据。但这主要是为了避免为此任务保留通用寄存器之一。

并不是说Linux不会在x86上使用分段,因为这是不可能的。您已经强调了一部分,Linux以非常有限的方式使用分段。第二部分是Linux仅在80x86体系结构需要时才使用分段

您已经列举了原因,分页更容易且更可移植。


7

图中的每个区域都是一个分段吗?

没有。

尽管分段系统(在x86上为32位保护模式)旨在支持单独的代码,数据和堆栈段,但实际上所有段都设置为相同的内存区域。也就是说,它们从0开始,到内存(*)的结尾。这使得逻辑地址和线性地址相等。

这称为“平面”内存模型,它比在其中具有不同段然后在其中包含指针的模型要简单一些。特别是,分段模型需要更长的指针,因为除了偏移量指针之外,还必须包括分段选择器。(16位段选择器+ 32位偏移量,总共48位指针;而只有32位平面指针。)

64位长模式除了平面内存模型外,实际上甚至不支持分段。

如果要在286上以16位保护模式进行编程,则将需要更多段,因为地址空间为24位,而指针仅为16位。

(*请注意,我不记得32位Linux如何处理内核/用户空间分离。分段可以通过设置用户空间段限制来实现这一点,以便它们不包括内核空间。分页允许这样做,因为它提供了每页保护级别。)

那么为什么说Linux不使用分段而是仅使用分页呢?

x86仍然具有这些段,您不能禁用它们。只是尽可能少地使用它们。在32位保护模式下,需要为平面模型设置段,即使在64位模式下,它们仍然存在。


呵呵,我想32位内核可能比通过更改CS / DS / ES / SS上的段限制来更改页面表更便宜,从而避免Meltdown,从而防止用户空间访问2G或3G以上。(Meltdown vuln是页表条目中内核/用户位的一种解决方法,它允许用户空间从仅映射内核的页面读取用户空间)。VDSO页面可能映射在4G的顶部,尽管:/ wrfsbase在保护/兼容模式下是非法的,只有长模式,所以在32位内核用户空间上不能将FS base设置为高。
彼得·科德斯

在64位内核上,32位用户空间可能会跳到64位代码段,因此您不能仅依靠段限制来实现Meltdown保护,而仅在纯32位内核中即可。(这在具有大量物理RAM的计算机上具有很大的劣势,例如,线程堆栈的低内存用完了。)无论如何,Linux通过分页保护内核内存,而普通用户空间中的base / limit = 0 / -1段(不是用于线程本地存储的FS / GS)。
彼得·科德斯

在硬件页表(PAE)中支持NX位之前,一些早期的安全补丁使用分段来为用户空间代码创建不可执行的堆栈。例如linux.com/news/exec-shield-new-linux-security-feature(Ingo Molnar的帖子提到“ Solar Designer的出色“非执行堆栈补丁””。)
Peter Cordes

3

Linux x86 / 32不使用分段,因为它将所有分段初始化为相同的线性地址和限制。x86体系结构要求程序具有段:代码只能从代码段执行,堆栈只能位于堆栈段中,数据只能在数据段之一中进行操作。Linux通过以相同的方式设置所有段(除了您的书中始终没有提到的例外)来绕过该机制,以便在任何段中相同的逻辑地址均有效。实际上,这等效于完全没有任何段。


2

图中的每个区域都是一个分段吗?

这是“段”一词的2种几乎完全不同的用法

  • x86分段/分段寄存器:现代x86操作系统使用平面内存模型,其中所有分段在32位模式下具有相同的base = 0和limit = max,与硬件在64位模式下强制执行的操作相同,从而使分段成为一种残余。(FS或GS除外,即使在64位模式下也用于线程本地存储。)
  • 链接器/程序加载器的部分/段。(ELF文件格式的段和段有什么区别

惯例有共同的起源:如果使用分段存储器模型(特别是在没有分页的虚拟存储器),则可能必须的数据和BSS的地址是相对于DS段的基础上,相对于SS基堆,并以相对码CS基址。

因此,可以在不更改相对于段基数的16或32位偏移的情况下,将多个不同的程序加载到不同的线性地址,甚至在启动后移动它们。

但是,随后您必须知道指针相对于哪个段,因此您有了“远指针”,依此类推。(实际的16位x86程序通常不需要将其代码作为数据访问,因此可以在某处使用64k代码段,也可以在DS = SS处使用另一个64k块,其中堆栈从高偏移量增长而出,数据位于底部。或者是所有段基都相等的微型代码模型)。


x86分段如何与分页交互

32/64位模式下的地址映射为:

  1. segment:offset(包含偏移量的寄存器所隐含的段基,或被指令前缀覆盖)
  2. 32或64位线性虚拟地址= base + offset。(在像Linux这样的平面内存模型中,指针/偏移量也=线性地址。除了相对于FS或GS访问TLS时。)
  3. 页表(由TLB缓存)线性映射到32(旧模式),36(旧PAE)或52位(x86-64)物理地址。(/programming/46509152/why-in-64bit-the-virtual-address-are-4-bits-short-48bit-long-compared-with-the)。

    此步骤是可选的:必须在启动期间通过将控制寄存器中的位置1来启用分页。没有分页,线性地址是物理地址。

注意,分割并不会让你在单个进程(或线程)使用的虚拟地址空间的超过32位或64位,这是因为扁平(线性)地址空间一切被映射到仅具有相同数目的位作为偏移量本身。(对于16位x86并非如此,分段实际上对于使用超过64k的内存(主要是16位寄存器和偏移量)很有用。)


CPU缓存从GDT(或LDT)加载的段描述符,包括段基。当取消引用指针时,根据指针所在的寄存器,默认情况下将DS或SS作为段。寄存器值(指针)被视为距段基的偏移量。

由于段基数通常为零,因此CPU会对此进行特殊处理。或者从另一个角度来看,如果你这样做有一个非零段基址,加载有额外的延迟,因为“特殊”(正常)绕过加入的基址不适用的情况。


Linux如何设置x86段寄存器:

在32位和64位模式下,CS / DS / ES / SS的基数和限制均为0 / -1。之所以称为平面内存模型,是因为所有指针都指向同一地址空间。

(AMD CPU架构师通过将平面内存模型强制为64位模式来避免分割,因为主流操作系统仍未使用它,除了no-exec保护外,后者通过PAE或x86分页以更好的方式提供- 64页表格式。)

  • TLS(线程本地存储):在长模式下,FS和GS 不会固定为base = 0。(它们是386的新增功能,没有被任何指令隐式使用,甚至没有rep使用ES 的-string指令)。x86-64 Linux将每个线程的FS基址设置为TLS块的地址。

    例如,mov eax, [fs: 16]将16字节的32位值加载到该线程的TLS块中。

  • CS段描述符选择CPU处于哪种模式(16/32/64位保护模式/长模式)。Linux对所有64位用户空间进程使用一个GDT条目,对所有32位用户空间进程使用另一个GDT条目。(为了使CPU正常工作,还必须将DS / ES设置为有效条目,SS也是这样)。它还选择了特权级别(内核(环0)与用户(环3)),因此,即使返回到64位用户空间,内核仍然必须安排CS更改,使用iretsysret代替常规跳转或退出指令。

  • 在x86-64中,syscall入口点用于swapgs将GS从用户空间的GS翻转到内核的GS,它用于查找该线程的内核堆栈。(线程本地存储的一种特殊情况)。该syscall指令不会更改堆栈指针以指向内核堆栈。当内核到达入口点1时,它仍然指向用户堆栈。

  • DS / ES / SS还必须设置为有效的段描述符,以使CPU在保护模式/长模式下工作,即使在长模式下忽略了来自这些描述符的基数/限制。

因此,基本上x86分段用于TLS,以及硬件要求您执行的强制性x86 osdev东西。


脚注1:有趣的历史记录:在AMD64芯片发布之前的几年中,内核开发人员和AMD架构师之间就有邮件列表邮件归档,从而对其设计进行了调整,syscall因此可以使用。有关详细信息,请参见此答案中的链接。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.