图中的每个区域都是一个分段吗?
这是“段”一词的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位模式下的地址映射为:
- segment:offset(包含偏移量的寄存器所隐含的段基,或被指令前缀覆盖)
- 32或64位线性虚拟地址= base + offset。(在像Linux这样的平面内存模型中,指针/偏移量也=线性地址。除了相对于FS或GS访问TLS时。)
页表(由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更改,使用iret
或sysret
代替常规跳转或退出指令。
在x86-64中,syscall
入口点用于swapgs
将GS从用户空间的GS翻转到内核的GS,它用于查找该线程的内核堆栈。(线程本地存储的一种特殊情况)。该syscall
指令不会更改堆栈指针以指向内核堆栈。当内核到达入口点1时,它仍然指向用户堆栈。
DS / ES / SS还必须设置为有效的段描述符,以使CPU在保护模式/长模式下工作,即使在长模式下忽略了来自这些描述符的基数/限制。
因此,基本上x86分段用于TLS,以及硬件要求您执行的强制性x86 osdev东西。
脚注1:有趣的历史记录:在AMD64芯片发布之前的几年中,内核开发人员和AMD架构师之间就有邮件列表邮件归档,从而对其设计进行了调整,syscall
因此可以使用。有关详细信息,请参见此答案中的链接。