首发于术道经纬
segmentation和保护模式(三)

segmentation和保护模式(三)

segmentation和保护模式(二)

80386保护模式时代

自从80386引入了paging后,segmentation其实已经没有什么存在的价值了,可以被取代了,然而intel为了保持向前兼容性,依然保留了segmentation机制,所以从80386到今天的x64处理器,地址转换的完整路径都是这样的:

而且,segmentation是必须存在的,不可以被disable,反倒paging是可选的(Long Mode下paging是必须的,参考这篇文章)。想想也是,要是没有segmentation只有paging,你让1982年的80286怎么办。

如果不使用paging的话,则linear address就直接是物理地址了;如果使用paging,从paging的角度,linear address就是虚拟地址(这也是为什么x86里虚拟地址不叫virtual address,而是叫linear addess)。

可是使用segmentation真的是多此一举诶,白白增加一次查表和转换的时间,那可不可以绕过这一步呢?

一个巧妙的方法就是将所有的segment register都指向同一个segment descriptor,然后让该descriptor里存的segment起始地址为0,这样所有的segment都重叠了,都是从0地址开始的,就等同于没有了,logical address也直接等于linear address了,这种内存划分被称为平坦内存模型(flat momery model),与之对应的是segmented memory model。

如果使用paging,并采用segmented memory model,则每个segment被进一步被划分成多个pages。采用flat memory model的话就更不用说了,基本等同于内存就是由pages组成的。Segmentation中的descriptor有各种权限检测位,paging中的descriptor也有各种权限检测位,两者一致还好办,如果不一致呢?

比如对于某个segment,其descriptor限定为该segment是只读的,但是这个segment里某个page的descriptor又将该page设为可写的。如果出现这种情况,则后一级page table descriptor里的设置,会覆盖掉前一级segment descriptor里对应的设置。规则变得复杂起来,这就是保持兼容性的代价。

64位长模式下的segmentation

所谓64位长模式,是指运行在64位的x86_64处理器上,且操作系统和应用程序都是64位的。在64位模式中,DS, ES, SS已经不再使用。对于FS和GS,也不会再进行segment descriptor中的limit检测和attribute检测,而只是进行一下地址的canonical检测。可以看出,在x86_64中,segmentation的使用已经被越来越弱化了。

从实模式到保护模式

MS-DOS系统是使用实模式的,但现在MS-DOS已经不怎么被使用了,Linux和Windows等现代操作系统都是运行在保护模式,那现在实模式还有用么,为什么现在最新的x64处理器依然支持实模式?

因为BIOS之类的引导程序还需要用到它。保护模式依赖于GDT/LDT和page table,而它们的建立和初始化,只可能在实模式下进行。就像一个可执行文件的代码不可能全部用C语言实现,因为C语言要用到函数,而函数的执行需要堆栈,堆栈的初始化只能用汇编语言实现。

x86上电后即进入实模式,置位CR0寄存器的PE位可进入保护模式(其实还需要其他一系列操作)。虽然通过将PE位置0可以退回实模式,但是额外需要非常多繁琐的操作。

如果你仅仅是出于某种原因不想要这套保护机制,那你可以首先关掉CR0中的WP(Write Protect)全局写保护,然后把所有的页表描述符中的U/S位置为user,R/W置为writable,那不就是表面上有保护,实际上等于没有了么,因为保护的就是只读的部分不要被错误写,属于supervisor的部分不要被user错误访问,全都是user+writable还有啥可保护的。是不是很tricky的操作……

segmentation和paging的区别

其实保护模式下的segmentation和paging机制真的是很相似的,一个是查GDT表,一个是查页表,但它们也存在一些区别:

  1. page是固定大小的(比如4KB),且大小由处理器架构决定,而segment的大小是不固定的,且大小由软件确定。因为segment的长度不定,在分配内存时,可能会发生内存中的空闲区域小于要加载的segment,导致分配失败,而粒度更小的page可以更好的利用这些空闲区域。

2. 在多进程的环境中,每个用户进程在运行的时候都希望有一个简单的执行环境,一个单一的地址空间(关于地址空间请参考这篇文章),好像自己占有整个计算机一样,而不用介入复杂的内存管理过程。

segmentation和paging都能解决地址空间隔离的问题,但segmentation不能解决内存使用效率的问题。segmentation对内存区域的映射是以segment为单位的,如果内存不足,被换出到磁盘的是整个segment,这势必会造成大量的磁盘访问操作,严重影响系统速度。

因此,segmentation还是因为粒度偏大而显得粗糙,而paging在换入和换出内存的时候是以page为单位,并由此衍生出了demand paging的page cache,demand allocation的anonymous pagespage relcaim等众多机制,提高了内存的使用效率。

番外 - Linux系统中的segmentation

Linux虽然支持segmentation,但对这一硬件特性的使用非常有限,一是因为segmentation和paging本来就是冗余的,二是因为linux作为一个通用的操作系统,需要兼容不同的硬件平台,而RISC架构一般是没有segmentation机制的,所以2.6版本的linux仅在针对x86平台的部分涉及到了segmentation。

Linux中对GDT的实现是,每个核有一个GDT,每个GDT包含18个segment descriptors,其中4个为user code, user data, kernel code, kernel data。它们的base都是0,所以其实也是flat memory model。区别仅在于user对应的两个segments的DPL是3,kernel对应的两个segments的DPL是0。


参考:

《程序员的自我修养——链接、装载和库》


原创文章,转载请注明出处。


编辑于 04-02

文章被以下专栏收录