这学期有了操作系统课程,并且加入了试点班。试点班的任务还挺实在的,一上来就是从硬件的引导,也就是在操作系统加载之前的MBR中代码的执行。
因为对于汇编不熟,所以代码里面有些细节还不太清楚,这篇文章主要记录一下引导的大致过程,承载引导信息与代码MBR,以及MBR之后的分区与文件系统。
由于水平有限,无法做到细节俱到,只是在此记录我对于整体过程的理解。
另外,这里只介绍MBR+MBR硬盘启动方式,并未考虑到UEFI+GPT硬盘的方案。
存储设备、分区与文件系统
存储设备
什么是一个存储设备?(这里并非指易失性存储,主存、缓存等)
在这里,我不想像学院派的计算机体系一样,将其只是作为对主存的辅助,因为在当今的计算机世界,硬盘与内存完全是两个角色,虽然这有可能是工业制造技术的原因,但至少这是事实;我也不想像一个硬件厂商那样,将存储设备细分为软盘、硬盘、USB设备等等,这些设备其实本质是相同的,我们在此需要考虑的是它们之间的“共相”,并非差异,它们的差异或许只在于硬件接口实现与历史遗留问题,不要让这些称为自己理解的累赘。
对我们来说,对一块存储设备,无论是软盘硬盘还是U盘,最本质的抽象是一个数组。将它们统统都当做数组,暂且忽略掉差异。与数组相伴而生的,就是数组的索引方式,对应的是存储设备中的寻址方式。接受了这个抽象,存储设备就成为了你理念世界中的一个个小方块了。
文件系统
对于一块硬盘,我们最直觉的感受是什么?是它能存储文件。想一想,一边是一个数组,另一边是一堆目录与文件,这两个东西怎么联系起来呢?用文件系统。如果考虑到文件也是一块字节串,那么文件系统本质上就是实现这个功能的: 在一块数组上,完成对一系列字节块的组织与操作。“组织”是数据结构,“操作”是算法,两个合起来就是文件系统。 我们在“格式化”一个“硬盘”的时候,做的就是“初始化数据结构”的工作,初始化后的数据结构就放在硬盘这个数组内部。之后,如果用户要存储文件,只要按照规定的算法,合理地操作这些数据结构,将自己要存的东西写进该写的地方就可以了。查询文件也是一样。
我们尝试体验一下这个过程。假设要实现存放文件的功能,不考虑性能。我们用最简单的方式来做:将整个数组分成大小相同的块,比如每块512个字节,每块存一个文件,每块的第一个字节存储这个块有没有被使用过,之后的七个字节存储这个块存的文件的文件名;要存储文件的时候,就从头以此寻找尚且没有被占用的块,查找的时候,就以此找对应的文件名。这样,一个简陋的、支持文件大小有限的、支持文件名非常短的文件系统就完成了。在初始化之后,我们只需要一段能跑起来的代码就可以使用它了。
虽然很简陋,但是大概也能说明问题。
分区
文件系统理解了,那分区的概念就没什么好说的了。
我们不满足于将一整块硬盘当做一个文件系统用,所以我们搞了一张表,叫做分区表,一般就在硬盘的开头。这个表的作用很简单,里面有四项,每一项对应一个分区,记录两个字段:分区的开始与结尾。这样子,整块硬盘就被分为了四个块,称作四个分区,也就是四个数组,我们在每个数组上都建立文件系统,就可以分块使用这个硬盘了。
以上描述中,硬盘一词可以替换为软盘、U盘等等。
分区表存储的位置见下面所述的MBR。
MBR与引导
在我们的个人电脑经过硬件加电、自检完成之后,它就要进行一些预备活动来给用户提供可使用的交互接口了,个人电脑上这个交互接口就是操作系统,而预备我们称之为引导。
引导时期会做什么事情呢?
-
加载
首先,硬件发现一个存储设备,如果我们将存储设备看做一个很大的数组,那么硬件就会将这个数组的前512个字节加载到内存的0000:7c00处;
这512个字节的内容就是我们说的主引导记录(Master Boot Record,MBR),按照约定,这里面大致包含了一个三个内容:引导代码、分区表、可启动标志。描述 长度 代码区 446 分区表 64 可启动标志 2 512 -
检查
之后,硬件去检查加载进来的内容的最后两个字节的内容,也就是可启动标志,如果是0x55aa,那么表明这个设备是一个可启动设备,否则,转而尝试其他设备; -
运行
确定该设备可启动之后,硬件就会尝试执行之前加载的MBR中的代码。这时候,系统的控制权就交给了引导代码,也就是我们写的代码。
上述过程很简单,在这个过程中需要注意的:
- 512个字节长的MBR都是由我们自己在代码里面写的,也就是说,我们的汇编代码里面不止需要有可执行的代码,还需要填充好其余的字节,比如启动标志和分区表。这也就是为什么我们需要在代码的末尾写上
times 512-($-$$)
,以及为什么最后要有0xaa55的原因,但由于暂时不需要分区表,所以分区表并没有被填充。 - MBR并非被加载到0地址处,而是7C00处,也就是说,我们的代码最终执行的时候起点会在内存的7C00处,这时候就需要注意了,如果在代码里我们用到了跳转指令等,那么汇编器在生成机器代码的时候,需要如何填充跳转目标呢?如果汇编器认为代码的起始地址是0,那么跳转指令的操作码就会是以0为基准计算得来的了,很显然会出问题,所以我们需要在代码最开始用org伪指令告诉汇编器程序的起点,这样,在通过偏移量计算绝对位置的时候,生成的代码才会不出问题。当然,如果代码里面没有用到通过偏移计算的,比如全是mov指令,那么无论如何都不会出问题。
处理好开头与结尾,剩下的就是如何写代码了。
参考: