Linux之系统启动流程(1)—X86的由来

date
Mar 9, 2023
slug
linux-0
author
status
Public
tags
Linux
summary
type
Post
thumbnail
KL_Intel_D8086.jpeg
category
📗 Docs
updatedAt
Mar 17, 2023 09:10 AM

计算机简史

 
说起计算机,不得不提一下计算机的最初形态,右图中是人类第一代计算机——— 真空管计算机,真空管是弗莱明在1904年发明的,使用玻璃外壳密封,里面装有碳丝和铜板,并抽成接近真空。具有单向导通的能力,是一种二极管。1907年,德福雷斯特在真空二极管的基础上发明了真空三极管,可通过栅极电压控制阴极到阳极之间的电流,也可以当作压控开关使用。
第一代计算机就是使用的真空管的技术,包括赫赫有名的ENIAC。ENIAC使用了17468个电子真空管,耗电功率约150千瓦。但真空管体积大,耗电量大,并不能进行长时间的工作。
 
                                  图一
图一
ENIAC的诞生跟二战有着莫大的关系。为了研制新型大炮及其他武器,美国陆军军械部在马里兰州设立了弹道研究实验室。为了解决每天面临的大量弹道计算问题,才有了1942年提出的试制“高速电子管计算装置”的设想,也才在1946年顺利建成ENIAC。
 
如今,计算机已经发展到第四代了,计算机的结构也发生了天翻地覆的变化,CPU从8008微处理器到8080再到8088、8086,再到现在动辄5nm工艺上亿晶体管的CPU也已经见怪不怪了,两年前我还在为个人电脑CPU超频到5Ghz欢呼雀跃,今年最新的CPU默频已经突破5Ghz。
 
不得不感叹时代在进步,每当我们以为现如今某些事物已经发展到尽头的时候,若干年后再回首发现那时候只不过才刚开始起步。作为一个普通人,我们可能无法引领这个时代,但是我们却可以站在巨人的肩膀上,去享受无数先驱者建立的智慧。
 

计算机架构

 
通常来讲,计算机架构包括以下三个部分:
  • 处理器
  • 内存
  • 外部设备
以上各部分均通过系统总线连接,系统总线由地址总线、数据总线和控制总线组成。下图描述了计算机体系结构:
                                                                         图二
图二
其中,最核心的部分就是CPU(Central Processing Unit),它是整个计算机的大脑。但是仅靠CPU也无法完成运算操作,CPU必须和内存配合工作,很多复杂的计算任务都需要将中间结果保存下来,然后基于中间结果进行进一步的计算。CPU 本身没办法保存这么多中间结果,这就要依赖内存了。
 
总线上的一些其他设备,例如显卡会连接显示器、磁盘控制器会连接硬盘、USB 控制器会连接键盘和鼠标等等。CPU 和内存是完成计算任务的核心组件,所以这里我们重点介绍一下 CPU 和内存是如何配合工作的。
 
学过计算机的同学都知道,CPU分为三个部分,运算单元、数据单元和控制单元。
 
其中运算单元负责做运算,比如加、位移等,但是它也仅仅只有“算”这个功能,至于算什么,算的结果给谁都不清楚。
 
运算单元计算的数据如果每次都要经过总线,到内存里面现拿,这样就太慢了,所以就有了数据单元。数据单元包括 CPU 内部的缓存和寄存器组,空间很小,但是速度飞快,可以暂时存放数据和运算结果。
 
控制单元可以在获得指令之后,执行指令。这个指令会指导运算单元取出数据单元中的某几个数据,计算出个结果,然后放在数据单元的某个地方。
                                                                                    图三
图三
我们以AB进程为例,来看下CPU的各个单元是如何协助的。
 
首先,控制单元分为四个模块:
  • 指令起始地址寄存器
  • 数据起始地址寄存器
  • 指令指针寄存器
  • 数据指针寄存器
 
实际上,我们的程序就是用若干个指令将数据运算成期望的样子,所以程序在编译成二进制之后,里面会包含一系列的指令。假如CPU的上现在的进程为进程A,那么指令起始地址寄存器里面存放的就是进程A的第一个指令在内存中的位置,而指令指针寄存器里面存放的则是一个地址偏移量(offset),它代表的是下一个指令在内存中的地址,另外两个寄存器也都是这个逻辑。
 
而指令分为两部分:一部分是做什么操作;一部分是操作哪些数据
 
要执行这条指令,就要把第一部分交给运算单元,第二部分交给数据单元。数据单元根据数据的地址,从数据段里读到数据寄存器里,就可以参与运算了。运算单元做完运算,产生的结果会暂存在数据单元的数据寄存器里。最终,会有指令将数据写回内存中的数据段。
 
所以,你也看出来了,CPU某一时刻只能“专注”为一个进程服务,如果想要运行B进程,就涉及到CPU中寄存器值的切换,这个就叫做“进程切换”。
 

地址/数据总线

 
CPU 和内存来来回回传数据,靠的就是总线,其实总线上主要有两类数据,一个是地址数据,也就是我想拿内存中哪个位置的数据,这类总线叫地址总线(Address Bus);另一类是真正的数据,这类总线叫数据总线(Data Bus)
 
而地址总线决定CPU能够访问数据的范围,我们都知道在计算机底层数据都是二进制的机器码,假如只有两位,那 CPU 就只能认 00,01,10,11 四个位置,超过四个位置,就区分不出来了。位数越多,能够访问的位置就越多,能管理的内存的范围也就越广。
 
而数据总线的位数,决定了一次能拿多少个数据进来。例如只有两位,那 CPU 一次只能从内存拿两位数。要想拿八位,就要拿四次。位数越多,一次拿的数据就越多,访问速度也就越快。
 

X86架构的由来

 
随着个人计算机的兴起,一款由IBM推出的使用微软的MS-DOS做操作系统,CPU用INTEL的8086的个人PC横扫了当时的个人PC市场,后面还被起诉,说其垄断市场。所以IBM不得不公开一些技术,使得后来无数 IBM-PC 兼容机公司的出现,也就有了后来占据市场的惠普、康柏、戴尔等等。
 
所以,当年INTEL的8086处理器架构先入为主,成了行业的开放事实标准。由于这个系列开端于 8086,因此称为 x86 架构。
 

8086原理

 
作为x86的始祖,至今操作系统的很多特性都和它有关,我们来看一下它的架构:
                                                                            图四
图四

8086数据单元部分

 
为了暂存数据,8086 处理器内部有 8 个 16 位的通用寄存器,也就是刚才说的 CPU 内部的数据单元,分别是 AX、BX、CX、DX、SP、BP、SI、DI。这些寄存器主要用于在计算过程中暂存数据:
 
  • AX 累加器(Accumulator),使用频度最高。
  • BX 基址寄存器(Base Register),常存放存储器地址。
  • CX 计数器(Count Register),常作为计数器。
  • DX 数据寄存器(Data Register),存放数据。
  • SP (Stack Pointer)栈指针,用来指向当前的栈。
  • BP (Base Pointer)基指针,用来保存使用局部变量的地址。
  • SI和DI,索引寄存器, 这两个寄存器通常用来处理数组或字符串
 
为了灵活性,AX、BX、CX、DX 可以分成两个 8 位的寄存器来使用,分别是 AH、AL、BH、BL、CH、CL、DH、DL,其中 H 就是 High(高位),L 就是 Low(低位)的意思。
 

8086控制单元部分

 
  • IP寄存器:就是上文中讲到的指令指针寄存器,指向代码段中下一条指令的位置。CPU 会根据它来不断地将指令从内存的代码段中,加载到 CPU 的指令队列中,然后交给运算单元去执行。
  • CS寄存器:代码段寄存器(Code Segment Register)。
  • DS寄存器:数据段的寄存器(Data Segment Register)。
  • SS寄存器:栈寄存器(Stack Register)。
 
其实CS就是指令起始指针寄存器,IP里记录的是一个偏移量,CS+IP就是一个具体指令地址。DS的偏移在通用寄存器中。
 

如何凑够20位地址

 
我们知道,CS和DS都是16位,偏移量也是16位的,但是8086的地址总线是20位,怎么去凑够这20位呢?方法就是“起始地址 *16+ 偏移量”,也就是把 CS 和 DS 中的值左移 4 位,变成 20 位的,加上 16 位的偏移量,这样就可以得到最终 20 位的数据地址。
 
notion image
 
所以8086处理器最多只能区分2^20=1M的内存地址,又因为偏移量只有16位,所以每次只能访问2^16=64K大小的段。
 

32位处理器

 
32位处理器,就是有32位地址总线,可以访问2^ 32=4G的内存。那么怎么向下兼容呢?
 
首先,通用寄存器有扩展,可以将 8 个 16 位的扩展到 8 个 32 位的,但是依然可以保留 16 位的和 8 位的使用方式。你可能会问,为什么高 16 位不分成两个 8 位使用呢?因为这样就不兼容了呀!
 
其中,指向下一条指令的指令指针寄存器 IP,就会扩展成 32 位的,同样也兼容 16 位的。
notion image
从上图中我们发现,除了段选择器,其他寄存器都是较之前名称前加了个E(Extend),然后扩充了16位。
 
上面我们讲到,在8086处理器中20位的地址总线是通过 CS 和 DS 中的值左移 4 位 + 16位的偏移量,那么也就意味着段的起始地址不能是任何一个地方,只是能整除 16 的地方。
 
因为之前是16位的,为了硬适配20位总线,必须先左移4位,那索性就改成32位的行不行呢,反正32位4G的内存都可以访问到了?
 
实际上解决办法并不是粗暴的直接扩展,而是为了适配性,CS、SS、DS、ES 仍然是 16 位的,但是不再是段的起始地址。段的起始地址放在内存的某个地方。这个地方是一个表格,表格中的一项一项是段描述符(Segment Descriptor)。这里面才是真正的段的起始地址。而段寄存器里面保存的是在这个表格中的哪一项,称为选择子(Selector)。
 
这样,将一个从段寄存器直接拿到的段起始地址,就变成了先间接地从段寄存器找到表格中的一项,再从表格中的一项中拿到段起始地址。这样段起始地址就会很灵活了。当然为了快速拿到段起始地址,段寄存器会从内存中拿到 CPU 的描述符高速缓存器中。这样就不兼容了,咋办呢?好在后面这种模式灵活度非常高,可以保持将来一直兼容下去。前面的模式出现的时候,没想到自己能够成为一个标准,所以设计就没这么灵活
 
在32位的架构下, 系统启动有两种模式可以切换,【实模式】直接获取到段的起始地址,即以前的实现方式;【保护模式】则是间接获取其实地址,即第二种实现方式 。
 
通过切换模式,来兼容历史,同时在保护模式下,又具备很强的扩展性。
 
notion image

总结

在了解X86架构之前,我花了很多时间去了解相关的各种资料。比如8086的原理,计算机发展史等,但大都是囫囵吞枣,整个知识体系异常庞大。 所以只能浅尝辄止,惭愧。
 
先通过这篇文章来初步了解一下X86架构,方便后边的学习,也不至于被人问到X86是什么的时候一脸懵逼了~~