26

全网最硬核讲解计算机启动流程

 3 years ago
source link: http://www.cnblogs.com/flashsun/p/13942138.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

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

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

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

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

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

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

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

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

一、为什么是 BIOS 主导?

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

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

二、内存映射

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

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

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

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

三、实模式下的内存分布

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

Bfa6v2R.png!mobile

哎哟我真是个小天使,把比例都表现出来了,网上能再找出比我这个更直观的请给我留言。实模式之后再解释,现在简单理解就是计算机刚开机的时候只有 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 这个位置

;程序主体

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. 启动区代码主要是加载操作系统内核,并跳转到加载处(四跳)

经过这连续的四次跳跃,终于来到了操作系统的世界了,剩下的内容,可以说是整个操作系统课程所讲述的原理,分段、分页、建立中断、设备驱动、内存管理、进程管理、文件系统、用户态接口等等。

这些名次在操作系统的课程中你可能都或多或少听过,如果你好好学了的话也一定知道大概的原理,不过像笔者这样从头到尾研读过 linux 内核源码的硬核狗来说,这些概念不只是书本上枯燥无味的概念,而是活灵活现在操作系统的每一行代码上,有的展现了作者无比的智慧,有的让我看到了作者由于硬件设定不得已做出的屈服,建议你也找时间读一读,与我交流一下感悟哈哈。

九、参考资料

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

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

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK