hurlex <二> 计算机启动过程、GRUB 以及 multiboot 标准
2014-09-07 posted in [hurlex开发文档]
GRUB的全称是GRand UnifiedBootloader,是一个多重操作系统启动管理器,用来引导不同的操作系统。Linuxer和喜欢折腾多系统的读者们想必对GRUB并不陌生吧?但是在详细描述GRUB之前,我们得先来简述一下操作系统的启动过程。本着别人写过的我们就不写的原则,我推荐来自阮一峰博客的一篇文章给大家《计算机是如何启动的?》。1
看过这篇文章之后,想必大家已经在宏观上对计算机的启动过程有了初步的了解。接下来我们细化这个过程,并且着重阐述其中和本项目相关的内容。一直没有说明的是,我们的目标内核是32位的。因为我们只是原理学习,64位繁杂的细节会使得整个项目难度加大,这是我不愿意看到的。2
我们先来一起复习计算机原理之类的课程中对于CPU寻址的一些概念吧。首先,我们的内核使用32位的地址总线来寻址,所以能编址出2的32次方,也就是4G的地址空间。那么第一个问题是,这4G的空间指向哪里?我想大多数读者的第一反应都是内存吧?我们知道在主板上除了内存还有BIOS、显卡、声卡、网卡3等外部设备,CPU需要和这些外设进行通信。那么实现通信自然就得有地址,不然怎么表示数据的去向呢?比如显卡内部就有自己的一些存储单元4。在x86下,当需要访问这些存储单元的时候,就需要给予不同的访问地址来区分每一个读写单元。
说到这里,我们需要引出两个专业名词:端口统一编址和端口独立编址。还记得我们刚说的4G地址空间吗?所谓的端口统一编址就是把所有和外设存储单元对应的端口直接编址在这4G的地址空间里,当我们对某一个地址进行访问的时候实际上是在访问某个外设的存储单元。而端口独立编址就是说这些端口没有编址在地址空间里,而是另行独立编址。而x86架构部分的采用了端口独立编址,又部分的采用了端口统一编址。部分外设的部分存储单元直接可以通过某个内存地址访问,而其他部分在一个独立的端口地址空间中,需要使用in/out指令去访问,我们用到的时候再来细说。
上文简单的介绍了一下地址空间的概念,接下来我们详细分析CPU在加电后的启动过程。这里可能比较枯燥和难以理解,但是没关系,这里的流程是固化的,程序员们能做的很有限。5我增加这一章只是为了读者们能够充分理解我们之后内容的原理,并没有和编程相关的东西,所以大家只要大致理解就好。
我们从按下电源开始。首先是CPU重置。主板加电之后在电压尚未稳定之前,主板上的北桥控制芯片会向CPU发出重置信号(Reset),此时CPU进行初始化。当电压稳定后,控制芯片会撤销Reset信号,CPU便开始了模式化的工作。此时形成的第一条指令的地址是0xFFFFFFF06,从这里开始,CPU就进入了一个“取指令-翻译指令-执行”的循环了。所以我们需要做的就是在各个阶段提供给CPU相关的数据,以完成这个“接力赛”。这个接力过程中任何一个环节如果出现致命问题,其导致的直接后果就是宕机。死机是最好的结果,最坏的结果是程序在“默默的”破坏我们的数据,所以一定要谨慎对待。
那么,这个地址指向哪呢?大家一定想到了,它指向BIOS芯片里。我们刚刚说过,在4G的地址空间里,有一些地址是分给外设的,这个地址便是映射到BIOS的。我们知道,计算机刚加电的时候内存等芯片尚未初始化,所以也只能是指向BIOS芯片里已经被“固化”的指令了。
紧接着就是BIOS的POST(Power On SelfTest,上电自检)过程了,BIOS对计算机各个部件开始初始化,如果有错误会给出报警音。当BIOS完成这些工作之后,它的任务就是在外部存储设备中寻找操作系统,而我们最常用的外存自然就是硬盘了。自己安装过操作系统的读者应该都设置过BIOS选项吧?BIOS里面就有一张启动设备表7,BIOS会按照这个表里面列出的顺序查找可启动设备。那么怎么知道该设备是否可以启动呢?规则其实很简单:如果这个存储设备的第一个扇区中512个字节8的最后两个字节是0x55和0xAA,那么该存储设备就是可启动的。这是一个约定,所以BIOS会对这个列表中的设备逐一检测,只要有一个设备满足要求,后续的设备将不再测试。
当BIOS找到可启动的设备后,便将该设备的第一个扇区加载到内存的0x7C00地址处,并且跳转过去执行。而我们要做的事情,便是从构造这个可启动的扇区开始。
因为一个扇区只有512字节,放不下太多的代码,所以常规的做法便是在这里写下载入操作系统内核的代码9,这段代码就是所谓的bootloader程序。一般意义上的bootloader负责将软硬件的环境设置到一个合适的状态,然后加载操作系统内核并且移交执行权限。而GRUB是一个来自GNU项目的多操作系统启动程序。它是多启动规范的实现,允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。
引出GRUB的原因很简单:我不准备自己实现bootloader程序。理由有二:其一,实现bootloader牵扯太多在后期才要讲述的知识。与其前期简陋的实现这个bootloader,还不如就用现成的优秀实现,以后有机会自己再学着改进;第二,我想在后面把这个小内核安装到物理机器上去,而读者们想必在自己的机器上已经有了多个操作系统了。这样的话如果非得实现自己的bootloader的话,势必会造成和已有操作系统的不兼容。所以,我干脆决定直接使用GRUB来加载内核。以后就能让它很简单的安装在物理机器上,这样的话我们能拥有一个Linux系统和自己的小内核共存的计算机了。如果你愿意的话,也可以再加上一个Windows系统。
听起来很不错吧?那么问题是怎么能让GRUB加载这个小内核呢?答案是GRUB提供的multiboot规范。这份规范是描述如何构造出一个能够被GRUB识别,并且按照我们定义的规则去加载的操作系统内核。目的很明确吧?具体的协议在网上很容易检索到,也有不少中文版本的翻译,所以我不再详细解释这个协议。希望对这个协议陌生的读者们先去网上得到一份具体的说明,仔细的阅读一遍后再阅读下一章节。在下一章节中,我们将自己动手构建出一个可以运行的“Hello,kernel OS”程序,拭目以待吧。
P.S. 古老的8086处理器是0xFFFF0这个地址。
其实你可以理解为我压根就不懂64位,所以不敢过多谈论。 ↩
这里就原谅我使用这些不专业的词汇吧。 ↩
甚至还有独立的GPU,我们这个简单的内核不用关注这个。 ↩
当然了,设计BIOS的程序员可以在这里大显身手。 ↩
对这个地址有疑问的话,请参考《Intel IA-32 Intel Architecture Software Developer’s Manual Volume 3:System Programming Guide Section》的9.1.4 章节。或者参考我博客的一篇文章《基于Intel 80×86CPU的IBM PC及其兼容计算机的启动流程》,地址是:http://www.0xffffff.org/?p=310↩
其实严格的说,数据保存在CMOS芯片里,BIOS存放的是程序代码。 ↩
不见得所有设备的扇区都是512字节,这篇文档我们只针对PC机而言,以后不再注释。 ↩
或者载入另一段代码,这段代码负责查找和载入内核。 ↩
原文: