【自制操作系统01】硬核讲解计算机的启动过程

本讲只为讲明白下面一个问题:

我们按下开机键后究竟发生了什么?

好的,这似乎是好多人都特别想搞明白的一个问题,有时候非常纳闷,为什么一个看似这么简单的问题,就是搜不到一个直面问题的答案呢?

好问题,我也不知道为什么会这样,但我猜是因为:

  • 其一,似懂非懂的人太多,他们其实也不知道究竟发生了什么,所以只能模糊大概地说一些教科书上的话。
  • 其二,知道这个答案的人一定是大牛,大牛要么不回答这个问题,要么就不会简单地回答这个问题。而我呢,自认为刚好处于两者之间,现在又特别想把自己知道的分享出来,所以你在这里找到了答案。

我想当你探寻这个问题的答案是,搜到的大多数是这样的描述:

BIOS按照“启动顺序”,把控制权转交给排在第一位的存储设备:硬盘。然后在硬盘里寻找主引导记录的分区,这个分区告诉电脑操作系统在哪里,并把操作系统被加载到内存中,然后你就能看到经典的启动界面了,这个开机过程也就完成了。

这种描述简直太魔幻了,为什么是 BIOS 主导这一切?怎么叫按照启动顺序?这个分区咋就被加载到内存了,有咋告诉电脑操作系统在哪里了?我无法忍受这样的魔幻描述,我非要把它说得清清楚楚。

首先学一个东西,一定要有一个前置的知识,我们把它当做已知的,我不可能从原子组成分子开始讲原理。那学习计算机启动过程的前置知识是什么呢?我要求你已知以下几点:

  1. 内存是存储数据的地方,给出一个地址信号,内存可以返回该地址所对应的数据。
  2. CPU 的工作方式就是不断从内存中取出指令,并执行。
  3. CPU 从内存的哪个地址取出指令,是由一个寄存器中的值决定的,这个值会不断进行 +1 操作,或者由某条跳转指令指定其值是多少。

好了,只需要知道这三点前置知识,你就能专业地解释计算机的启动过程了。

一、为什么是 BIOS 主导?

都说开机后,BIOS 就开始运行自己的程序了,又硬件自检,又加载启动区的。我就不服了,为什么开机后是执行 BIOS 里的程序?为啥不是内存里的?为啥不是硬盘里的?

好的,不要怀疑前置知识,CPU 的工作方式,就是不断从内存中取指令并执行,那为什么会说是执行 BIOS 里的程序呢?这就不得不说说内存映射了。

二、内存映射

CPU 地址总线的宽度决定了可访问的内存空间的大小。比如 16 位的 CPU 地址总线宽度为 20 位,地址范围是 1M。32 位的 CPU 地址总线宽度为 32 位,地址范围是 4G。你可以算算我们现在的 64 位机的地址范围。

可是,可访问的内存空间这么大,并不等于说全都给内存使用,也就是说寻址的对象不只有内存,还有一些外设也要通过地址总线的方式去访问,那怎么去访问这些外设呢?就是在地址范围中划出一片片的区域,这块给显存使用,那块给硬盘控制器使用,等等 。

这样说,其实就不符合我们的前置知识了,所以可以有一种不太正确的理解方式,那就是内存中的这块位置就是显存,那块位置就是硬盘控制器。我们在相应的位置上读取或者写入,就相当于在显存等外设的相应位置上读取或者写入,就好像这些外设的存储区域,被映射到了内存中的某一片区域一样。这样我们就不用管那些外设啦,关注点仍然是一个简简单单的内存。这就是所谓的内存映射

太好了,现在又用简单的前置知识就能解释得通了,我们继续往下推。

三、实模式下的内存分布

刚刚说到内存中划分出了一片一片区域给各种外设,那么问题自然就来了,哪块区域,分给了哪块外设了呢?如果是规定,那应该有一张表比较好吧。嗯没错,还真有,它就是实模式下的内存分布,笔者给它画了一张图:

在这里插入图片描述

哎哟我真是个小天使,把比例都表现出来了,网上能再找出比我这个更直观的请给我留言。实模式之后再解释,现在简单理解就是计算机刚开机的时候只有 1M 的内存可用。

我们看到,内存被各种外设瓜分了,即映射在了内存中。BIOS 更狠,不但其空间被映射到了内存 0xC0000 - 0xFFFFF 位置,其里面的程序还占用了开头的一些区域,比如把中断向量表写在了内存开始的位置,真所谓先到先得啊。

四、怎么就从 BIOS 里的程序开始执行了

好了,现在我们知道 BIOS 里的信息被映射到了内存 0xC0000 - 0xFFFFF 位置,其中最为关键的系统 BIOS 被映射到了 0xF0000 - 0xFFFFF 位置。假如我现在说,CPU 开机就是执行了这块区域的代码,然后巴拉巴拉一顿操作就开机了,你肯定要喷我了,为什么就执行到这了呢,那咋不从头开始执行?

这就自然有了一种猜想,我们要用到另一个前置知识了,就是 CPU 从内存的哪个位置取出执行并执行呢?是 PC 寄存器中的地址值。BIOS 程序的入口地址也就是开始地址是 0xFFFF0(人家就那么写的),也就是开机键一按下,一定有一个神奇的力量,将 pc 寄存器中的值变成 0xFFFF0,然后 CPU 就开始马不停蹄地跑了起来。没错,接下来这句话,可能就是你找了很久的答案,请做好准备:

在你开机的一瞬间,CPU 的 PC 寄存器被强制初始化为 0xFFFF0。如果再说具体些,CPU 将段基址寄存器 cs 初始化为 0xF000,将偏移地址寄存器 IP 初始化为 0xFFF0,根据实模式下的最终地址计算规则,将段基址左移 4 位,加上偏移地址,得到最终的物理地址也就是抽象出来的 PC 寄存器地址为 0xFFFF0。

当我在学习这段知识时,看到这句话才让将我心里积压了很久的疑惑解开,多么简单粗暴的道理啊。写到这里我也是长舒了一口气,因为剩下的过程,就几乎只是流水账一样的正推了。

至于怎么强制初始化的,我觉得就越过了前置知识的边界了,况且各个厂商的硬件实现也不一定相同,有很多办法,也很简单。讨论起来意义就不大了。

五、BIOS 里到底写了什么程序

好了,我们现在知道了 BIOS 被映射到了内存的某个位置,并且开机一瞬间 CPU 强制将自己的 pc 寄存器初始化为 BIOS 程序的入口地址,从这里开始 CPU 马不停蹄地向前跑了起来。那接下来的问题似乎也非常自然地就问出来了,那就是 BIOS 程序里到底写了啥?

把 BIOS 程序里的二进制信息全贴出来也不合适,我们分析一些主要的。我们首先还是来猜测,你看入口地址是 0xFFFF0,说明程序是从这执行的。实模式下内存的下边界就是 0xFFFFF,也就是只剩下 16 个字节的空间可以写代码了,这够干啥的呢?如果你有心的话应该能猜出,入口地址处可能是个跳转指令,跳到一个更大范围的空间去执行自己的任务。没错就是这样,0xFFFF0 处存储的机器指令,翻译成汇编语言是:

jmp far f000:e05b

意思是跳转到物理地址 0xfe05b 处开始执行(回忆下前面说的实模式下的地址计算方式)。

地址 0xfe05b 处开始,便是 BIOS 真正发挥作用的代码了,这块代码会检测一些外设信息,并初始化好硬件,建立中断向量表并填写中断例程。这里的部分不要展开,这只是一段写死的程序而已,而且对理解开机启动过程无帮助,我们看后面精彩的部分,也就是 BIOS 的最后一项工作:加载启动区

六、0x7c00 是啥

该较真的地方就是要较真,我绝对不会让加载这种魔幻的词出现在这里,我们现在就来把它拆解成人话。

其实这个词也并不魔幻,加载在计算机领域就是指,把某设备上(比如硬盘)的程序复制到内存中的过程。那加载启动区这个过程,翻译过来就是,BIOS 程序把启动区的内容复制到了内存中的某个区域。好了,问题又自然出来了,启动区是哪里?被复制到了内存的哪个位置?然后呢?我们一个个来回答。

什么是启动区呢?即使你不知道,你也应该能够猜到,一定是符合某种特征的一块区域,于是人们把它就叫做启动区了,那要符合什么特征呢?先不急,不知道你有没有过设置 BIOS 启动顺序的经历,通常有 U 盘启动、硬盘启动、软盘启动、光盘启动等等,BIOS 会按照顺序,读取这些启动盘中位于 0 盘 0 道 1 扇区的内容

至于磁盘格式的划分,本篇就不做讲解了,总之对于内存,我们给出一个数字地址就能获取到该地址的数据,而对于磁盘,我们需要给出磁头、柱面、扇区这三个信息才能定位某个位置的数据,都是描述位置的一种方式而已。

接着说, 这 0 盘 0 道 1 扇区的内容一共有 512 个字节,如果末尾的两个字节分别是 0x55 和 0xaa,那么 BIOS 就会认为它是个启动区。如果不是,那么按顺序继续向下个设备中寻找位于 0 盘 0 道 1 扇区的内容。如果最后发现都没找到符合条件的,那直接报出一个无启动区的错误。

BIOS 找到了这个启动区之后干嘛呢?哦,前面说过了是加载,就是把这 512 个字节的内容,一个比特都不少的全部复制到内存的 0x7c00 这个位置。怎么复制的?当然是指令啦。哪些指令呢?这里我只能简单说指令集中是有 in 和 out 的,用来将外设中的数据复制到内存,或者将内存中的数据复制到外设,用这两个指令,以及外设给我们提供的读取方式,就能做到这一点啦。

启动区内容此时已经被 BIOS 程序复制到了内存的 0x7c00 这个位置,然后呢?这个其实也不难猜测,启动区的内容就是我们自己写的代码了,复制到这里之后,就开始执行呗,之后我们的程序就接管了接下来的流程,BIOS 的使命也就结束啦。所以复制完之后,接下来应该是一个跳转指令吧!没错,正是这样,PC 寄存器的值变为 0x7c00,指令开始从这里执行。

咦?不知道你有没有发现,我们似乎不知不觉又把之前的一句魔法语言翻译成人话了,开头我们说:

BIOS 把控制权转交给排在第一位的存储设备。

所以这句话是什么意思呢?就是 BIOS 把启动区的 512 字节复制到内存的 0x7c00 位置,并且用一条跳转指令将 pc 寄存器的值指向 0x7c00。你看,这不是也没多几个字嘛,就把这个问题说得明明白白,简简单单。

哦,对了,现在似乎就剩下一个问题了,为什么非要是 0x7c00 呢?好问题,当然答案也很简单,那就是人家 BIOS 开发团队就是这样定的,之后也不好改了,不然不兼容。为什么不好改?我们看一个简单的启动区 512 字节的代码。(代码摘抄自《30天自制操作系统》)

; hello-os
; TAB=4

		ORG		0x7c00			;程序加载到内存的 0x7c00 这个位置
		
; FAT12格式软盘专用

		JMP		entry
		DB		0x90	;
		DB		"HELLOIPL"		;启动区名称(8字节)
		DW		512				;每个扇区大小(必须为512字节)
		DB		1				;簇大小(必须为1个扇区)
		DW		1				;FAT的起始位置(一般从第一个扇区开始)
		DB		2				;FAT的个数(必须为2)
		DW		224				;根目录大小(一般为224项)
		DW		2880			;该磁盘的大小(必须为2880扇区)
		DB 		0xf0			;磁盘的种类(必须是0xf0)
		DW		9				;FAT的长度(必须是9扇区)
		DW		18				;1个磁道有几个扇区(必须为18)
		DW		2				;磁头数(必须是2)
		DD		0				;不使用分区,必须是0
		DD		2880			;重写一次磁盘大小
		DB		0,0,0x29		;意义不明,固定
		DD		0xffffffff		;卷标号码
		DB		"HELLO-OS   "	;磁盘的名称(11字节)
		DB		"FAT12   "		;磁盘格式名称(8字节)
		RESB 	18				;空出18个字节

;程序主体

entry:
		MOV		AX,0			;初始化寄存器
		MOV		SS,AX
		MOV		SP,0x7c00
		MOV		DS,AX			;段寄存器初始化为 0
		MOV		ES,AX
		MOV		SI,msg
putloop:
		MOV		AL,[SI]
		ADD		SI,1
		CMP		AL,0			;如果遇到 0 结尾的,就跳出循环不再打印新字符
		JE		fin
		MOV		AH,0x0e			;指定文字
		MOV		BX,15			;指定颜色
		INT		0x10			;调用 BIOS 显示字符函数
		JMP		putloop
fin:
		HLT
		JMP		fin
msg:
		DB		0x0a,0x0a		;换行、换行
		DB		"hello-os"
		DB		0x0a			;换行
		DB		0				;0 结尾
	
		RESB 0x7dfe-$			;填充0到512字节
		DB	0x55, 0xaa			;可启动设备标识

我们看第一行:

ORG		0x7c00

这个数字就是刚刚说的启动区加载位置,这行汇编代码简单说就表示把下面的地址统统加上 0x7c00。正因为 BIOS 将启动区的代码加载到了这里,因此有了一个偏移量,所以所有写启动区代码的人就需要在开头写死一个这样的代码,不然全都串位了。

然后正因为所有写操作系统的,启动区的第一行汇编代码都写死了这个数字,那 BIOS 开发者最初定的这个数字就不好改了,否则它得挨个联系各个操作系统的开发厂商,说唉我这个地址改一下哈,你们跟着改改。在公司推动另一个团队改个代码都得大费周折,想想看这样的推动得耗费多大人力。况且即使改了,之前的代码也都不兼容了,这不得被人们骂死啊。

再看最后一行:

DB	0x55, 0xaa

这也验证了我们之前说的这 512 字节的最后两个字节得是 0x55 0xaa,BIOS才会认为它是一个启动区,才会去加载它,仅此而已。

回过头来说 0x7c00 这个值,它其实就是一个规定死的值,但还是会有人问,那必然有它的合理性吧。其实,我的解释也只能说是人家规定了这个值,后人们替他们解释这个合理性,并不是说当初人家就一定是这样想的,就好比我们做语文的阅读理解题一样。

第一个 BIOS 开发团队是 IBM PC 5150 BIOS,当时被认为的第一个操作系统是 DOS 1.0 操作系统,BIOS 团队就假设是为它服务的。但操作系统还没出,BIOS 团队假设其操作系统需要的最小内存为 32 KB。BIOS 希望自己所加载的启动区代码尽量靠后,这样比较“安全”,不至于过早的被其他程序覆盖掉。可是如果仅仅留 512 字节又感觉太悬了,还有一些栈空间需要预留,那扩大到 1 KB 吧。这样 32 KB 的末尾是 0x8000,减去 1KB(0x400) ,刚好等于 0x7c00。哇塞,太精准了,这可以是一种解释方式。

七、启动区里的代码写了啥

其实写到这,我这篇文章就应该戛然而止了,因为最初的那个问题已经解决了,CPU 已经开始马不停蹄地从我们预期的位置跑起来了,万事开头难,剩下的内容,就是操作系统想怎么玩就怎么玩了。

但我觉得还不够味,似乎还有些问题萦绕在你脑海里。比如说这个问题:

启动区里的代码写了啥?就 512 字节就是全部操作系统内容了?

这是一个好问题,512 个字节确实干不了啥,现在的操作系统怎么也得按 M 为单位算吧,512 个字节远远不够呢,那是怎么回事呢?

其实我们可以按照之前的思路猜测,BIOS 用很少的代码就把 512 字节的启动区内容加载到了内存,并跳转过去开始执行。那按照这个套路,这 512 字节的启动区代码,是不是也可以把更多磁盘中存储的操作系统程序,加载到内存的某个位置,然后跳转过去呢?

没错,就是这个套路。所以 BIOS 负责加载了启动区,而启动区又负责加载真正的操作系统内核,这配合默契吧?

由于用于启动盘的磁盘是人家写操作系统的厂商制作的,俗称制作启动盘,所以他也肯定知道操作系统的核心代码存储在磁盘的哪个扇区,因此启动区就把这个扇区,以及之后的好多好多扇区(具体取决于操作系统有多大)都读到内存中,然后跳转到开始的程序开始的位置。跳转到哪里呢?这个就不像 0x7c00 这个数那么经典了,不同的操作系统肯定也不一样,也不用事先规定好,反正写操作系统的人给自己定一个就好了,别覆盖其他关键设备用到的区域就好。

八、操作系统内核写了啥

好了现在经过好几轮跳跳跳,终于跳到内核代码啦,我们来一起回顾一下:

  1. 按下开机键,CPU 将 PC 寄存器的值强制初始化为 0xffff0,这个位置是 BIOS 程序的入口地址(一跳)
  2. 该入口地址处是一个跳转指令,跳转到 0xfe05b 位置,开始执行(二跳)
  3. 执行了一些硬件检测工作后,最后一步将启动区内容加载到内存 0x7c00,并跳转到这里(三跳)
  4. 启动区代码主要是加载操作系统内核,并跳转到加载处(四跳)

经过这连续的四次跳跃,终于来到了操作系统的世界了,剩下的内容,可以说是整个操作系统课程所讲述的原理。写到这,我真的可以结束了,可是,我还是感觉对不起我的读者。

所以操作系统内核究竟写了啥?其实我也没研读过源码,只是知道大概的流程罢了,有几个可能你一直困惑的问题我想在这里和你聊聊。

软硬件协同发展

有几个概念我想放在一块说,中断、分段、分页。这有啥关系呢?我想说,他们的共性就是他们都是软件与硬件相结合的产物。

  • 中断,软件提供了中断向量表和中断程序,而硬件留给我们一个 IDTR 寄存器来存储中断向量表的起始地址,并且提供了硬件机制来传递中断信号。
  • 分段,软件提供了段表和端描述符,而硬件留给我们一个 GDTR 寄存器来存储段表的起始地址,并且提供了硬件机制,将段基址寄存器里的值通过查段表来和段偏移地址进行拼接形成物理地址,并且提供了段保护机制。
  • 分页,软件提供了一级页表和二级页表,以及提供缺页中断异常的处理程序。而硬件留给我们一个 PTBR 寄存器来存储页表的其实地址,并且提供了硬件机制,将逻辑地址转换为物理地址,记录页的访问次数,并在适当的时候发出缺页中断异常。

所以你看,他们放在一起是不是很工整呢?所以有很多人有这样的误区,认为某个技术只是软件来实现的,或者只是硬件来实现的,那必然怎么想都想不通。

历史遗留问题

还有一些你可能觉得很别扭的概念,比如说实模式保护模式

觉得别扭就对了,因为有些计算机问题是历史遗留问题,并不是合理发展的产物。最初的 x86 架构的 CPU 是 8086,它是 16 位机,所有的寄存器都是 16 位的,只有地址线是 20 位的,可以访问 1M 的地址空间。它访问空间的方式是段基址寄存器左移四位,加上段偏移地址,形成一个物理地址,送到地址线上。

这有两个坏处,第一就是可访问的空间太小,跟不上计算机发展的需要。第二就是缺少保护机制,程序可以随意访问这 1M 的地址空间,甚至覆盖掉操作系统本身的代码和数据。

这样操作系统的发展和需求,就倒逼着 CPU 的发展,于是有了新的 32 位 CPU。32 位 CPU 地址空间扩大到了 4G,也通过段选择子的方式对内存进行了保护,有了很多新的特性。但为了兼容老程序,必须还要支持 16 位机的特征,所以就有了两个模式,并且程序还要通过调用一些 CPU 的指令从一个模式切换到另一个模式,比如经典的打开 A20 地址线

而这两个模式,其实我们完全可以按照更准确的理解,叫做 16 位模式和 32 位模式,然后补充一句说 32 位模式比 16 位模式有了更大的寻址空间(这是自然的)和更好的内存保护机制。但由于 CPU 厂商希望凸显他们新模式的优势,所以直接把最关键的优势“保护”给放到了名字里,叫保护模式。而为了对比也要给之前的模式起一个名字,之前的模式比较实在,给出什么地址就直接是物理地址,也没有转换也没有安全保护,那就叫它实模式吧。

所以,这个命名方式,以及这是历史遗留问题而妥协的设计方式,成为了好多人理解这两种模式的障碍,总觉得不能见名知意是自己没有理解到位,感觉别别扭扭的就说明自己没有了解到它的好处。其实除了兼容老版本外,根本没有一点好处,重新设计 32 位机才是最干净的做法。

历史遗留问题还有很多,我敢说所有你觉得设计的很别扭的地方,可能大概率都是历史遗留问题,比如说段基址寄存器的存在,如果不是因为当初 16 位机器却有 20 位地址线,可能就没有这种蹩脚的设计了。不过好在后来把它用在了段保护机制上,才有了点作用,可毕竟最初的设计,只是为了妥协呀。了解历史因素,还是有好处的。

九、参考资料

好了,这回我真的要结束了,相信如果你真的看完了全文,计算机的启动过程,可以说有了比较具象的了解。如果你想深入细节,也就是了解整个过程的每一点,那可要下功夫了。

初学者推荐两本书籍,可以顺序阅读:

  • 《30 天自制操作系统》
  • 《操作系统真象还原》

十、开源项目和课程规划

如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们(下方有公众号和小助手微信),一起来开发。

项目开源

项目开源地址:https://gitee.com/sunym1993/flashos

当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。

如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

posted @ 2020-01-20 17:41  闪客sun  阅读(7324)  评论(10编辑  收藏  举报