前言:
操作系统的特权级模块在整个操作系统的学习中应该算的上是最难啃的了,提到特权级就要绕不开保护模式下的分段机制;如果想要彻底弄明白就要对比实模式下的分段机制有什么缺陷。这就衍生出很多问题如:什么是实模式?采用分段机制进行寻址的意义?什么是保护模式?为什么要设计保护模式?…?
如果只是针对性的单独回答某个问题一直在罗列八股文要点,只讲“是什么”而忽略“为什么”,初学者看完之后会感觉始终没学透彻,更适合与有基础的人复习。任何事物发展到今天,都有段“合理”的过程,了解这个过程是怎么来的,有助于理解它今天的形态。在“处理器的发展史”中分段机制、保护模式等每一个机制的出现都是为了解决当时的一个重大问题,要想彻底把特权级模块啃透,就必不可免要去了解一下“处理器的发展史”(之所以是“处理器的发展史”而不是“操作系统的发展史”,是因为分段机制、保护模式、特权级这些概念都是处理器的工程师们设计的,操作系统只是一个协助者和应用者,后面会解释)。很多文章在梳理“处理器的发展史”的过程中无法抓住重点,花费大量篇幅讲解不关键的点,让人读起来容易分散精力,甚至读完以后忘记为啥点进来了。比如:为什么要采用分段机制进行寻址?“从分段机制首次出现的8086 处理器开始讲起,花费大量篇幅介绍8086 处理器是如何利用分段机制 在16位寄存器的基础上使用20位地址总线进行寻址的。” 却忽略了分段机制引入的关键是为了解决程序动态重定向问题,或者一笔带过。在计算机的发展史中每一种新机制的引入都是为了解决一个或多个痛点,我们要抓住最痛的那个点,因为不到万不得已是不会引入新机制的,充分利用20位地址总线的问题显然增加寄存器的容量为20位来的更简单,并不是引入分段机制的核心原因。文章开始前给大家推荐三本书:操作系统真相还原(强烈推荐)、30天自制操作系统、深入理解Linux内核
接下来我们就沿着“处理器的发展史”来看下操作系统的特权级机制是如何发展的。首先我们挑选两个里程碑:1.分段机制首次出现;2.保护模式首次出现
在看 CPU 的实模式和保护模式的文章之前,建议大家先花费十几分钟时间,看下这几篇处理器相关文章:因为无论 CPU 在哪种模式下工作,核心工作原理是不变的,有了这一思想武装起来后再讲模式就简单多了。
CPU与指令集、自研指令集难吗?、CPU如何执行指令以及流水线技术
本篇主要是围绕第一个里程碑来讲解“实模式”下的分段机制。首选介绍一下什么是“实模式”,CPU 中本来是没有实模式这一称呼的,是因为有了保护模式后,为了与老的模式区别开来,所以称老的模式为实模式。就好比汽车中的自动挡和手动挡,本来是没有手动挡这个概念的,因为后来有了自动挡汽车,先前的汽车就被称为手动挡汽车了。
在“处理器的发展史” 分段机制首次出现在Intel的8086CPU中。不知道大家有没有这样的疑虑这都2023年了还用8086这样的古董CPU讲解知识是否太落伍了,学到的知识在现代CPU中能用得上嘛。这个请大家放心,Intel 8086 CPU 应该称为80x86,它是Intel x86指令集架构下的第一款CPU,其中的86代表的是x86指令集体系,自那以后的CPU称为 286 386 486 586……即使现在的”酷睿“、”奔腾“也都属于x86系列。所以道理是不变的,而且用最简单的 80x86 CPU 学习,更容易理解和看透 CPU 运行机制。
8086之所以称为CPU界的里程碑就是因为它引入了分段机制来进行内存访问。在它之前的CPU对内存的访问比8086还有“实诚”,它们没有段的概念,程序要计算机上运行必须采用静态重定位的方式将内存地址“硬编码“写死在代码中,这导致程序首次装入内存后程序在内存中的位置就不可移动了,操作系统无法再次对程序进行重定位操作。
什么是静态重定向?我们用高级语言编写的源代码想要机器上运行需经历 编辑、编译、链接、装入、运行五个阶段,要想在CPU上运行就要遵循CPU的规则,在8086之前的CPU没有”段“的概念,CPU直接以真实物理地址访问内存,没有任何花哨,要在CPU上运行的程序必须直接使用包含真实物理地址的指令。首先编译和链接阶段由于编译器和链接器不能确定程序装入内存后的真实物理地址,所以产生的机器指令都采用以“0x00000”作为参考地址的相对地址也称为逻辑地址,等待程序首次装入内存时操作系统的装入程序模块会对目标程序中的地址相关的机器指令进行修改,将逻辑地址转换为真实的物理地址,这个过程就是静态重定向。列入:程序装入前机器指令 mov eax,0x00100 ,代表将逻辑地址0x00100中的数据"666"装入寄存器eax,程序装入时操作系统分配了以0x0c000为起始位置的物理内存,此时"666"对应的真实物理地址为0x0c100,因此上述指令会被修改为 mov eax,0x0c100 ,这样程序就能准确无误的在CPU上运行了但此后程序内存中的位置就不可移动了,因为指令中的物理地址已经不会再被改变了,如果程序移动了就会出现不可知异常。
明白了静态重定向,我们来看下仅仅依靠静态重定向的程序在运行时有什么弊端。在静态重定向机制下程序首次装入内存后就不能再次在内存中移动了,在这个前提下再牛逼的操作系统也无法高效的完成内存的回收和分配。导致本就不富裕的内存空间还会产生大量的外部碎片,无法被充分利用。如下场景,有可用内存160KB,但由于程序A位置不可移动,程序B虽然仅需100KB内存却无法运行。
上述囧况让本不富裕的内存雪上加霜,Intel早期的工程师难以承受内心的自责,不顾自己满头白发,熬了无数个通宵后,终于发明了“段”,从此CPU访问内存采用“段+偏移”的形式,地址重定位由静态重定位变为动态重定位。
怎么理解动态重定位?动态重定位不需要在装入阶段进行指令的修改,而是在执行阶段每次CPU访问内存时进行逻辑地址到物理地址的转换,动态重定位不需要装入程序模块的软件支持,需要CPU的硬件支持。引入段基址寄存器,用于保存程序被分配的起始物理地址即段基地址,编译和链接后生成的逻辑地址即段内偏移地址。程序运行过程中CPU在访问内存时会将段基址寄存器中的段基地址与段内偏移地址相加得到真正的物理地址。
如上图程序在装入内存后地址相关的机器指令不会被修改,依旧使用编译链接后的逻辑地址,逻辑地址与物理地址的转换是在执行包含内存寻址相关指令时由硬件完成的。转换的方式就是取段基址寄存器中的段基地址与逻辑地址相加的结果进行寻址。如若操作系统想要移动程序在内存中的位置只要将段基址寄存器中的段基地址更新为新位置的起始物理地址即可。
在分段机制下程序可以在内存中随意移动,这给了操作系统在内存管理方面很大发挥空间。例如:上述内存充足,但碎片化导致程序B无法运行的囧况就可以通过内存压缩的方法将内存碎片合并。
处理器工程师还在分段机制的基础上将每个运行时程序所占内存分解成了:代码段+数据段+栈段+BSS段+堆段,将连续逻辑地址空间分散到多个非连续的物理内存空间中,这也是内存分配的一种优化,可以合理利用内存碎片如下图。
处理器工程师还为每个内存段设计了专门的段基址寄存器用来储存段基址如:cs、ds、ss、es、fs、gs。
- 代码段:代码段简而言之就是把所有指令都连续排放在一起,形成了一个全部都是指令的区域,里面储存的是指令的操作码及寻址方式等。该区域可以在硬盘上的文件中,也可以是被加载后的内存中,总之是一段指令区域。它们内部都是紧凑挨着的,内容形式完全一样,只是存放的介质不一样而已。CPU专门提供了CS段寄存器用来保存指向这个块区域的起始地址。CS寄存器中的起始地址加上IP寄存器中的段内偏移地址为CPU提供了导航功能。CPU执行到何处完全要听这两个寄存器的安排。
- 数据段:数据段和代码段类似,只是这段区域中的内容不是指令,而是存粹的数据,也就是说里面存储的是程序运行所需要的数据,属于指令中的操作数。CPU专门提供了DS段寄存器用来储存这段区域的起始地址。
- 栈段:栈段是在内存中,硬盘文件中没有,一般的栈段是由操作系统分配指定的,所以是程序被加载到内存后才有的,栈段算是一种特别的数据段,和普通数据段不同的是栈段起始地址是高地址,向低地址方向扩展。所以CPU专门提供了SS段寄存器用来储存这块区域的起始地址。
代码段(cs)、数据段(ds)、栈段(ss)寄存器从名字上就比较容易理解,那es、fs、gs这三个附加段寄存器是干吗的?其实就是多给大家提供几个段寄存器用而已,可以作为BSS段和堆段的段基址寄存器,多几个寄存器用不是更好嘛,省的紧紧巴巴的,纯粹是为了方便大家。
以上就是分段策略出现的原因,它就是首次在8086上出现,自那之后的CPU都是用这类思想访问内存,只是在形式上有小改动,所以8086如此极负盛名。随着技术的发展虽然后来设计出了分页机制,它可以更好的管理内存,提高内存的利用率和内存交互性能。但任何事物发展到今天,都有段“合理”的过程,虽然分段机制并不完美,但它依然是处理器发展史上的一个里程碑,由于x86架构的CPU都会向前兼容,所以分段机制时至今日依然被保留了下来,目前CPU大都采用分段+分页机制并存的段页式内存管理机制,CPU在进行内存寻址前先将逻辑地址通过分段机制转换成虚拟地址(线性地址),虚拟地址再经过分页机制转换成真实的物理地址,当然也可以和 Linux 操作系统那样让每一段的基地址都设为 0 ,这样就等于“绕开”了段机制。