# 2. 计算机体系结构与引导流程
# 2.1 引导流程
译者注:
这里将 boot 翻译为引导,boot 的含义为将操作系统加载到主存中并运行。
现在,让我们开始我们的旅程吧。
当我们重新引导我们的计算机时,我们必须重启它。启动的最开始阶段计算机上没有任何关于操作系统的概念,然后通过某种方式,它必须从当前已连接到计算机上的某种(例如:软盘,硬盘,usb等)固定储存设备上加载操作系统,任何操作系统的发行版都行。
我们马上就会发现,在载入操作系统之前的环境中我们的计算机只提供了极少的系统服务:在这一阶段即使是一个简单的文件系统也是很奢侈的(比如磁盘上逻辑文件的读写),幸运的是,我们有Basic Input/Output Software (BIOS)基本输入输出系统,这是一组软件程序集合,当计算机开机的时候从芯片中加载到内存中并完成初始化。BIOS 提供计算机上关键设备的自检和基本控制能力,例如;显示器、键盘和硬盘。
在BIOS完成对硬件的基本检测后,特别是安装的内存是否能正常的工作,它必须引导储存在你的设备中的操作系统了。这里我们我们需要了解,BIOS不能简单从硬盘中加载代表操作系统的文件。因为BIOS没有文件系统的概念。BIOS必须从硬盘设备上特定的物理地址上读取数据的特定扇区(通常是512B),例如:柱面(Cylinder) 2, 磁头 (Head) 3, 扇区 (Sector) 5 (磁盘寻址将在第3.6节详细介绍).
所以,对于BIOS来说找到OS最简单的位置就是某个磁盘的第一个扇区(例如 柱面 0, 磁头 0, 扇区 0),这个扇区通常被称为 boot sector(引导扇区),因为有些磁盘并没有包含操作系统-这些磁盘可能只是简单的提供额外的储存,所以对于BIOS来说,区分某个磁盘上的引导扇区是需要执行的引导代码还是普通的数据很重要,注意CPU不能区分代码和数据,它们都能被翻译为CPU指令,代码就是一些指令,编程者使用这些指令编写有用算法。
继续,BIOS采用了一个简单的方法:通过规定目标引导扇区的最后2个字节必须设置为魔法数字 0xaa55. 所以BIOS会遍历每一个存储设备(软盘、硬盘、CD 等),将他们的引导扇区的代码读取到内存中, 然后让CPU开始执行它发现的第一个以魔法数字结束的引导扇区, 这就是我们获取计算机控制权的地方。
译者注:
这里将 magic number 翻译为魔法数字, magic number 一般指常量,通常指定义者给定的固定数,而其他人并不知道为什么是这个固定的数,使用者按照定义者给定的方法使用就行了。
# 2.2 BIOS, 引导区,魔法数字
如果我们有二进制编辑器,例如 TextPad 或者GHex, 我们可以使用这些编辑器来编写原始的字节到文件中,而不是像标准文本编辑器会转化字符,例如将'A'转化为ASCII值,这样我们可以为自己制作一个简单并且有效的引导扇区。
e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 *
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa
图表 2.1: 一个机器代码的引导扇区,每一个字节都是用16进制数表示。
注意在表2.1中,有3个重要特点:
- 最开始的三个字节用16进制表示分别是 0xe9,0xfd和0xff 实际上都是CPU生产商定义的机器代码指令,用于无条件跳转的。
- 最后2个字节 0x55和0xaa组成了魔法数字,这些数字告诉BIOS这确定是一个引导区而不是碰巧位于引导扇区的数据。
- 文件中填充了0('*'表示为简洁起见省略了0),主要上、是为了将 BIOS 魔法数字定位在512字节磁盘扇区的末尾。
关于字节序我们需要特别注意:你可能会疑惑我们之前表述魔法数字的时候是用16位值 0xaa55 表示的,而我们在引导扇区写的却是Ox55和0xaa 两个连续的字节。这是因为 x86 结构是使用小端格式(低字节序)来处理多字节值的。与我们通常所熟悉的数字系统相反,小端格式是将低位字节放到高位字节的前面。 假如我们的系统发生了端格式的转换,即使我的账户里面只有0000005磅,我也能马上退休,或许还可以给前百万富翁基金捐赠几磅。
编译器和汇编器通过让我们指定数据的类型来隐藏很多端格式的问题。比如说,一个16位的值能够自动序列化为机器代码,并且他的字节顺序是正确的。
但是了解字节序有时是很有用的,特别是当我们查找bug 的时候能够确切的知道单个字节在储存设备或内存中的位置,所以字节序是很重要的。
这可能是你的计算机能运行的最简单的程序,但是这是有效的程序,我们可用通过下面2个方式来测试,其中第二种方式对于我们的实验来说更安全和合适。
- 使用你当前的操作系统允许的任何方式来将这这段启动块代码写入一个不重要的储存设备(软盘或者闪存)的第一个扇区,然后重启计算机。
- 使用虚拟机软件,类如:VMWare或者VirtualBox 等,将这段启动块代码作为虚拟机的磁盘镜像,然后启动虚拟机。
如果你的计算机启动后没有显示类如:“No operating system found” 这样的没有发现操作系统的提示,而是简单的挂着,你就可以确认你的代码已经被装载并执行了。这是我们放置到代码开始部分的无限循环代码起的作用,如果没有这个无限循环指令CPU将会崩溃, 一直执行内存中启动扇区后续的指令,这些指令大多是随机的且没有初始化的字节。直到它陷入无效的状态或者重启或者偶然发现并运行BIOS然后格式化主磁盘。 记住,计算是我们编写代码控制的,计算机只是机械的取指并执行指令直到关机。所以我们必须确保计算机执行的是我们精心设计的代码而不是内存中某处的随机数据字节。在这种底层水平上,我们对计算机拥有很多的强力的能力和责任,所以我们需要学习如何控制它。
# 2.3 CPU 模拟
使用Bochs 或者 Qemu 这样的CPU 模拟器是第三种测试我们底层程序的方式。通过这种方式我们不需要不断的重启计算机或者冒着丢失磁盘数据的风险。不像VMware和virthualBox 这样的机器虚拟化,它们通过在宿主机CPU上直接运行客户指令的方式来优化性能,模拟器使用程序来仿真一个特定的CPU架构,使用变量来模拟CPU 的寄存器,使用高级程序控制结构来模拟底层跳转指令等。因此模拟器的速度比较慢,但是通常更适合开发和调试相应的系统。
注意,为了让模拟器工作起来,你需要以一个磁盘镜像文件的形式给他一些代码让它运行。镜像文件就是简单的原始数据(像机器代码和数据),通常被写入硬盘,软盘,CDROM,USB等介质中。实际上,某些仿真器是从CDROM下载或提取映像文件的,并从这些文件成功启动并运行实际的操作系统,虽然虚拟化更适合这种场景使用。
模拟器将底层显示设备指令转化为像素并渲染到桌面窗口上。然后就可以在真实显示器上看到你渲染后的内容。
通常,对于文档内的练习文件也一样,可以确定的是,任何机器代码只要能正确的运行在模拟器上,也将会正确的运行再真实机器上,显然真实的机器上将跑得更快。
# 2.3.1 bochs: A x86 CPU 模拟器
Bochs 需要我们在本地目录下设置一个简单的配置文件-bochsrc, 用来描述设备(显示器和键盘等)将被怎么样模拟的细节,更重要的是描述当模拟器在启动时使用哪个磁盘镜像启动。 图表2.2 显示了一个简单的Bochs 配置文件,我们可以将 2.2 部分写的代码保存为boot_sect.bin,然后使用这个配置文件来测试引导扇区。
# Tell bochs to use our boot sector code as though it were
# a floppy disk inserted into a computer at boot time.
floppya: 1_44=boot_sect.bin, status=inserted
boot: a
Figure 2.2 简单的Bochs 配置文件
在Bochs上测试引导扇区,在终端输入:
$bochs
作为一个简单的实验,让我们试着修改我们的启动扇区上的BIOS 魔法数字为一个无效的数字,然后重启 Bochs. 由于 Bochs 对CPU 的模拟接近真实情况,所以再Bochs 中测试完代码后,你应该能够再真实计算机上启动它,并运行得更快。
# 2.3.2
Qemu 与 Bochs 类似,它更方便并且能够模拟x86架构以外的架构。 但是qemu 的文档没有Bochs 完善。qemu 运行很简单,不需要配置文件:
$qemu <your-os-boot-disk-image-file>
# 2.4 十六进制表示法的优势
我们已经看到过一些使用16进制的例子了,所以了解为什么在底层编程 中通常使用16进制是很重要的。
首先考虑一下为什么使用十进制计数对于我们来说如此的自然,可能会有帮助,因为当我们第一次看到16进制数时我们总是会问自己:为什么不简单的使用十进制呢?我不是这方面的专家,我将做一个假设,使用十进制与大多数人的手指总数为10有关,这导致了使用10个不同的符号表示数字的想法:0,1,2,。。。8,9.
十进制的底数是10(即有10个不同的数字符号),而十六进制的底数是16,所以我们需要创造一些新的数字符号;偷懒的方法就简单的使用几个字母来表示:0,1,2,...8,9,a,b,c,d,e,f, 例如这里的d符号表示十进制的13数字。
为了将十六进制系统与其他的数字系统区别开,我们通常使用0x作为前缀,或者使用h作为后缀,对于刚好没有包含任何字母数字的16进制数这很重要,比如0x50 不等于十进制的50 -- 实际上0x50 是十进制的80.
实际上,计算机将数子表示为位的序列(二进制数),因为根本上电路只能区分2种电状态:0和1--就像计算机一共只有2个手指,所以为了表示大于1的数字计算机将一系列位组合在一起,就像我们通过使用2个或以上的数字来表示大于9的数一样(例如456, 23)。
为了让我们对处理的数字的交流和商定容易一些,我们给一些特定长度的位序列取了特殊的名称。大多数计算机的指令最少处理8位值,我们称8位为一个字节(bytes),其他的还有short, int, long 分别通常代表16位,32位,64位的值。我们也见到过 字(word) 这个单词,用于描述CPU当前模式下最大的处理单元的大小。所以在16位实模式下,一个字代表16位值,而在32位保护模式下一个字代表32位值,等等。
所以再回头来看16进制的优势:位字符串写起来相当的耗时,比起与自然的十进制系统之间的相互转换,二进制与十六进制相互转换更容易。本质上是因为我们可以将转换分解为较小的4位二进制片段,而不是将所有的位加起来总计。这对于较大的位字符串很难(例如16,32,64 等)。图表2.3中的事例清晰的显示了十进制转换的困难。

图表 2.3: 将1101111010110110 转换位十进制数和十六进制数