硬盘
硬盘
硬盘发展简史
1956 年 9 月,世界上诞生了第一台磁盘存储设备 IBM 350 RAMAC(Random Access Method of Accounting and Control),没错,又是 IBM,这就是蓝色巨人的魅力,在几十年前已经为科技领航。此设备用磁头来读写数据,用盘片来存储数据,以后的硬盘都是以这样的模式发展。这个磁头可以直接移动 到盘片上的任何一块存储单元,从而实现了随机存储。虽然它的总容量只有 5MB,但是限于当时的制造 工艺水平,一共使用了 50 个直径为 24 英寸的盘片,摞起来的体积相当于冰箱那么大。
1968 年由 IBM 首次提出了“温彻斯特/Winchester”的技术,这可是个了不起的技术。“温彻斯特”技术的精髓是:“镀磁盘片在密封空间中高速自转,磁头悬浮在盘片上方,固定在磁头臂上沿盘片径向移动”。
磁头不与盘片接触也不应该接触,这是最容易想象的,如果磁头与盘片接触,在高速转动下摩擦,什么材料都会磨损,数据自然就丢啦。另外,盘片自转速度是存取数据速度的关键,如果磁头与盘片接触, 受摩擦力的影响,想快也快不了。
一个可行的方案是让磁头在盘片上方“悬浮”,与盘片保持非常近的距离, 类似咱们物理实验中的气垫导轨。盘片高速旋转会产生气流,磁头在这种气流下像飞碟一样悬浮,这样就保证了不会与盘片有摩擦。磁头被固定在磁头臂上,它能沿盘片径向移动,由于磁头和盘片各自的运动,再加上如此近的距离,所以哪怕一点灰尘都会造成磁盘的损伤。于是,磁头、盘片被密封在了一个盒子里。今天的硬盘依然是这样的结构。
世界上第一块基于 “温彻斯特”技术的硬盘,在 1973 年诞生, 它就是所有硬盘的源头IBM 3340,容量60MB,由两个30MB的存储单元拼合而成。从此硬盘技术的发 展便有了成形的结构基础。所以,今天的硬盘也称为温盘,在一般的可编程中断控制器上连接硬盘的引脚 都标有温彻斯特硬盘呢。
硬盘的随机存取是靠磁头臂不断移动实现 的,磁头臂移动到目标位置的时间称为寻道时间,如果存储的数据不连续,这一块那一片的,磁头就得不断调整 位置,这是机械式硬盘不可避免的,这便是硬盘的瓶颈所在,所以一般的硬盘都将寻道时间作为重要参数。
在众多竞争对象当中,也有一款顽强地活了下来,它就是 SSD 固态硬盘,人家也有几十年的历史了。 SSD 固态硬盘为我们带来了全新的解决方案,看起来就像大块的内存条。
存在即合理,尽管温彻斯特硬盘为了提速,内部加了很多缓存,应用了各种优化寻道的方法,甚至接口已经变成了串口,但主流磁盘转速还是 7200 转,速度却提升有限。于是 固态硬盘的优势开始显山露水,其最大的优点是速度快,但缺点也明显,容量低,价格也很贵。主要还是运用在要求存储速度较快的生产环境中如数据库系统。当然现在随着固态硬盘价格越来越便宜,也进入了咱们老百姓的家中。
硬盘的工作原理
为了讲清楚硬盘的工作原理,书的作者用画图板花了 1 个小时画了这张示意图,希望对大家有所帮助。
左边的主轴上有两张盘片,其实不止2张,这里示意性的画了两张。盘片固定在主轴上随主轴告诉转动,目前主流个人电脑硬盘上的转速是7200转/分钟。每个盘片分上下两面,每面都存储数据,每个盘面都由一个磁头来读取数据,所以一个盘面上对应2个磁头。
由于盘面与磁头是一一对应的关系,故用磁头号来表示盘面。磁头编号从上到下以 0 开始计数,所以用磁头 0 代表第 一个盘面。磁头不会自己在盘片上移动,它需要被固定在右 边的磁头臂上,在磁头臂的带动下,沿着盘片边缘向圆心的 方向来回摆动,注意,摆动的轨迹是个弧,并不是绝对径向地直来直去。一方面这是因为磁头臂是由步进电机驱动的, 磁头臂一端是步进电机主轴,另一端是磁头。步进电机每次都会转动一个角度,所以带动磁头臂在“画圆”,而磁头位 于磁头臂的另一端,所以也跟着呈钟摆运动,运动轨迹是个弧线,并不是直线。所以,图中磁头臂中标注了“类似于”径向运动,这就是“类似”的原因。
另一方面,磁头读取数据也不需要做直来直去的移动,能否找到数据,只跟它最终落点有关,和中间路径形状是没关系的。所以,一方面盘片的自转,另一方面磁头的摆动,这两种动作的合成,使磁头能够读取盘片任意位置的数据。
说完了硬盘内部各部件的运动,再说下存储逻辑。盘片表面是用于存储数据的磁性介质,为了更有效管理磁盘,这些磁性介质也被“格式化”成易于管理的格局,即将整个盘面划分为多个同心环,以圆心画扇形,扇形与每个同心环相交的弧状区域作为最基本的数据存储单元。
这个同心环就称为磁道,而同心环上的弧状区域是扇形的一部分,故称之为扇区,它作为我们向硬盘存储数据的最基本单位,大小是 512 字节。
我们写入的数据最终是写进了磁道上的扇区中。注意啦,我上面说的磁道是个环,不是线,很多教科 书上介绍磁道时都简单画了个圆圈,这容易让人误解磁道是条线,线上可无法存储数据,“环”是有横截 面的,数据就存储这些“面积”中。磁头臂带动磁头在盘片上方移动,就是在找磁道的位置,盘片高速自转,就是在磁道内定位扇区。看图,配合着想像力理解一下。
磁道的编号和磁头一样也是从 0 开始的。相同编号的磁道组成的管状区域就称为柱面。图中, 两个盘片上编号相同的磁道,它们之间用灰色直线连接起来的部分,很像柱子的弧形表面,所以称柱面。 如果盘片非常多的话,“柱面”就显得非常形象了。
CPU与外设通信—IO接口#南桥-输入输出控制中心中提到了,CPU只会通过IO接口与外设通信,那么硬盘的IO接口就是硬盘控制器。硬盘控制器同硬盘的关系,如同显卡和显示器一样,它们都是专门驱动外部设备的模块电路,CPU只同他们说话,由它们将CPU的话转译给外部设备,这是它们的共同点。不过,不同的是显卡和显示适配器是分开的,因为显示器太大了,不能塞到机箱里,但是现在的硬盘和硬盘控制器是在一起的,为什么用但是这个词呢,是因为很早之前硬盘控制器和硬盘也是分开的,不过业界内几个大佬合作开发出的新的接口,这才将硬盘和硬盘控制器整合到一起,这种接口便称为集成设备电路(Integrated Drive Electronics,IDE)。
随着 IDE 接口标准的影响力越来越广泛,全球标准化协议将此接口使用的技术规范归纳成为全球硬盘标准,这样就产生了 ATA(Advanced Technology Attachment)。不过由于 IDE 这个名字已经叫开了,所以大家依然习惯称硬盘为 IDE 硬盘。
计算机发展非常快,新老交替的现象层出不穷,前几年刚出道的硬盘串行接口(Serial ATA,SATA),由于其是串行,所以之前的 ATA 接口只好称为并行 ATA,即(Parallel ATA,PATA)。以前一般的主机只支持 4 个并口硬盘,但自从出现串口硬盘后, 情况就变了,支持多少块硬盘,取决于主板的能力。有的主板同时兼容这两种接口,如图所示。
!
这两种线缆完全不同,左边 PATA 接口的线缆也称为 IDE 线,一个 IDE 线上可以挂两块硬盘,一个是主盘(Master),一个是从盘(Slave)。一个主板支持这样的 4 块 IDE(PATA)硬盘,所以主板上提供两个 IDE 插槽。这两个接口也是以 0 为起始编号的,一个称为 IDE0,另一个称为 IDE1。不过按 ATA 的 说法,这两个插槽称为通道,IDE0 叫作 Primary 通道,IDE1 叫作 Secondary 通道。即使主板上安装的是 SATA 硬盘,它也兼容 PATA 的编程接口,向上兼容是计算机能源源不断向前发展的根基。所以,后面给出的端口号也将按照 PATA 这两个通道来分组给出。
这里所说的主盘 master、从盘 slave 别和 Primary 通道、Secondary 通道搞混了,通道是 channel,不是 disk,每个通道上分别有主盘和从盘。
硬盘控制器端口
接下来的内容是关于如何控制硬盘了。让硬盘工作,我们需要通过读写硬盘控制器的端口,端口的概念在此重复一下,端口就是位于IO控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。
下面图标中列出了部分端口,对于我们今后的应用,这几个端口足够了。
端口可以被分为两组,Command Block registers和Control Block registers。Command Block registers 用于向硬盘驱动器写入命令字或者从硬盘控制器获得硬盘状态,Control Block registers 用于控制硬盘工作状态。在 Control Block registers 组中的寄存器已经精减了,重点介绍 Command Block registers 组中的寄存器。
端口是按照通道给出的,不要误以为端口是直接针对某块硬盘的,不是这样的,一个通道上的主、从两块硬盘都用这些端口号。要想操作某通道上的某块硬盘,需要单独指定。上面的表格中就又一个叫 device 的寄存器,顾名思义,指的就是驱动器设备,也就是和硬盘相关。不过此寄存器是 8 位的,一个通道上就两块硬盘,指定哪一个硬盘只用 1 位就够了,寄存器资源宝贵不能浪费, 所以此寄存器是个杂项,很多设置都需集中在此寄存器中了,其中的第 4 位,便是指定通道上的主或从硬 盘,0 为主盘,1 为从盘。
端口用途在读硬盘和写硬盘时还是有点区别的,比如拿Primary通道上的0x1F1端口来说,读操作时, 若读取失败,里面存储的是失败状态信息,所以称为 error 寄存器,并且 0x1F2 端口中存储未读的扇区数。 写操作时就变成了 feauture 寄存器,此寄存器用于写命令的参数。
这么做的目的还是因为节省寄存器的成本…前辈们为了节省资源真的是用尽了奇技淫巧。
寻址方式 CHS 和 LBA
硬盘中的扇区在物理上是用“柱面-磁头-扇区”来定位的(Cylinder Head Sector),简称为 CHS。
但每次我们要事先算出扇区是在哪个盘面,哪个柱面上,这太麻烦了,这对于磁头来说很直观,它就是根据这 些信息来定位扇区的。可是咱们还是希望有一套对人来说较直观的寻址方法,我们希望磁盘中扇区从 0 开 始依次递增编号,不用考虑扇区所在的物理结构。其实我在描述需求时已经说出了 LBA 的定义,这是一 种逻辑上为扇区址的方法,全称为逻辑块地址(Logical Block Address)。
LBA有两种模式:
- LBA28模式:LBA28,用 28 位比特来描述一个扇区的地址。最大寻址范围是 2 的 28 次方等 于 268435456 个扇区,每个扇区是 512 字节,最大支持 128GB。我们这里为图简单,采用 LBA28 模式。
- LBA48模式:由于 128GB 已经不能满足日益增长的存储需求,硬盘越来越大了,得有相匹配的寻址方法与之配套,于 是要介绍的另外一种是 LBA48,用 48 位比特来描述一个扇区的地址,最大可寻址范围是 2 的 48 次方, 等于 281474976710656 个扇区,乘以 512 字节后,最大支持 131072TB,即 128PB。
硬盘中各寄存器的作用
按照上表硬盘_硬盘控制器主要端口寄存器来逐一介绍下寄存器的作用。
data 寄存器在名字上我们就知道它是负责管理数据的,它相当于数据的门,数据能进,也能出,所以其作用是读取或写入数据。数据的读写还是越快越好,所以此寄存器较其他寄存器宽一些,16 位(已经很不错了,表中其他寄存器都是 8 位的)。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口, 数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。
读硬盘时,端口 0x171 或 0x1F1 的寄存器名字叫 Error 寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,尚未读取的扇区数在Sector count寄存器中。在写硬盘时,此寄存器有了别的用途,所以有了新的名字,叫 Feature 寄存器。有些命令需要指定额外参数,这些参数就写在 Fea ture 寄存器中。 强调一下,error 和 feature 这两个名字指的是同一个寄存器,只是因为不同环境下有不同的用途,为了区别这两种用途,所以在相应环境下有不同的名字。这两个寄存器都是 8 位宽度。
Sector count 寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值 减 1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。这是 8 位寄存器,最大值为 255,若 指定为 0,则表示要操作 256 个扇区。
刚刚在[寻址方式 CHS 和 LBA](#寻址方式 CHS 和 LBA)中介绍完了 LBA,现在可以说 LBA 寄存器了,这里有 LBA low、LBA mid、LBA high 三个,它们三个 都是 8 位宽度的。LBA low 寄存器用来存储 28 位地址的第 0~7 位,LBA mid 寄存器用来存储第 8~15 位, LBA high 寄存器存储第 16~23 位。哎?三个 8 位的加起来才 24 位,连 LBA28 都不够,咱们怎么用呢?
有问题就有解决方案,这就引出了下一个寄存器,device 寄存器。
device 寄存器是个杂项,它的宽度是 8 位。在此寄存器的低 4 位用来存储 LBA 地址 的第 24~27 位。结合上面的三个 LBA 寄存器。第 4 位用来指定通道上的主盘或从盘,0 代表主盘,1 代 表从盘。第 6 位用来设置是否启用 LBA 方式,1 代表启用 LBA 模式,0 代表启用 CHS 模式。另外的两位: 第 5 位和第 7 位是固定为 1 的,称为 MBS 位,大家不用关注啦。
在读硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 Status,它是 8 位宽度的寄存器,用来给出硬盘的 状态信息。第 0 位是 ERR 位,如果此位为 1,表示命令出错了,具体原因可见 error 寄存器。第 3 位是 data request 位,如果此位为 1,表示硬盘已经把数据准备好了,主机现在可以把数据读出来。第 6 位是 DRDY, 表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。第 7 位是 BSY 位,表示硬盘是否繁忙,如果为 1 表示硬盘正忙着,此寄存器中的其他位都无效。另外的 4 位暂不关注。
在写硬盘时,端口 0x1F7 或 0x177 的寄存器名称是 command,和上面说过的 error 和 feature 寄存器情况 一样,只是用途变了,所以换了个名字表示新的用途,它和 status 寄存器是同一个。此寄存器用来存储让硬 盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。在咱们的系统中,主要使用了三个命令:
- identify: 0xEC,即硬盘识别。
- read sector:0x20,即读扇区。
- write sector:0x30,即写扇区。
在此只列出了本书需要的指令,大家若对此感兴趣,还是去看 ATA 手册,您懂的,里面内容丰富详实,相信大家一定会一饱眼福。
总结下寄存器 error、feature 和 status、command,大家可以这样来助记:这两组都是同一寄存器(也就是同一端口)多个用途,对同一端口写操作时,硬盘控制器认为这是个命令,对同一端口读操作时,硬盘控制器认为是想获得状态。
常用的硬盘操作方法
硬盘中的指令很多,各指令的用法也不同。有的指令直接往 command 寄存器中写就行了,有的还要 在 feature 寄存器中写入参数,最权威的方法还是要去参考 ATA 手册。由于本书中用到的都是简单的指令, 所以对此抽象出一些公共的步骤仅供参考之用。
不管是读硬盘,还是写硬盘,都不是一个指令就完事的。相关寄存器都需要设置。要是读硬盘,得告 诉读哪个扇区,读几个扇区,用哪种模式对扇区寻址,LBA?CHS?写硬盘也一样,写哪个,写几个,还要设置操作的是哪个通道的哪个硬盘……讲了这么多寄存器,心想,我到底先设置哪个寄存器呢?有没有个一般硬盘操作的基本顺序呢?其实最主要的顺序就是 command 寄存器一定得是最后写,因为一旦 command 寄存器被写入后,硬盘就开始干活啦,它才不管其他寄存器中的值对不对,一律拿来就用,有问题的话报错就好啦。其他寄存器顺序不是很重要。
- 先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
- 往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。
- 往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4位,选择操作的硬盘(master 硬盘或 slave 硬盘)。
- 往该通道上的 command 寄存器写入操作命令。
- 读取该通道上的 status 寄存器,判断硬盘工作是否完成。
- 如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
- 将硬盘数据读出。
硬盘工作完成后,它已经准备好了数据,咱们该怎么获取呢?一般常用的数据传送方式如下。
- 无条件传送方式。
- 查询传送方式。
- 中断传送方式
- 直接存储器存取方式(DMA)
- I/O处理机传送方式
对于上面的第 1 种“无条件传送方式”,应用此方式的数据源设备一定是随时准备好了数据,CPU 随时取随时拿都没问题,如寄存器、内存就是类似这样的设备,CPU 取数据时不用提前打招呼。
第 2 种“查询传送方式”,也称为程序 I/O、PIO(Programming Input/Output Model),是指传输之前, 由程序先去检测设备的状态。数据源设备在一定的条件下才能传送数据,这类设备通常是低速设备,比 CPU 慢很多。CPU 需要数据时,先检查该设备的状态,如果状态为“准备好了可以发送”,CPU 再去获取数据。硬盘有 status 寄存器,里面保存了工作状态,所以对硬盘可以用此方式来获取数据。
第 3 种“中断传送方式”,也称为中断驱动 I/O。上面提到的“查询传送方式”有这样的缺陷,由于 CPU 需要不断查询设备状态,所以意味着只有最后一刻的查询才是有意义的,之前的查询都是发生在数据尚未准备好的时间段里,所以说效率不高,仅对于不要求速度的系统可以采用。可以改进的地方是如果数据源设备 将数据准备好后再通知 CPU 来取,这样效率就高了。通知 CPU 可以采用中断的方式,当数据源设备准备好数据后,它通过发中断来通知 CPU 来拿数据,这样避免了 CPU 花在查询上的时间,效率较高。
第 4 种“直接存储器存取方式(DMA)”。在中断传送方式中,虽然极大地提高了 CPU 的利用率,但 通过中断方式来通知 CPU,CPU 就要通过压栈来保护现场,还要执行传输指令,最后还要恢复现场。似乎有同学说此方式已经很爽了,你还想怎样?哈哈,其实更爽的是一点都不要浪费 CPU 资源,不让 CPU 参与传输,完全由数据源设备和内存直接传输。CPU 直接到内存中拿数据就好了。这就是此方式中“直接”的意思。不过 DMA 是由硬件实现的,不是软件概念,所以需要 DMA 控制器才行。
第 5 种“I/O 处理机传送方式”。不知大家发现了没有,在说上面每一种的时候都把它们各自说得特别好,似乎完美不可替代了,但该公司一出新产品,就开始自曝曾经无与伦比的老一代产品的问题以突显现在产品更胜一筹。DMA 已经借助其他硬件了,CPU 已经很轻松了,难道还有更爽的方式?
是啊,DMA 方式中 CPU 还嫌爽的不够,毕竟数据输入之后或输出之前还是有一部分工作要由 CPU 来完成的,如数据交换、组合、校验等。 如果 DMA 控制器再强大一点,把这些工作帮 CPU 做了就好了。也是哦,既然为了解放 CPU,都已经引用一个硬件(DMA)了,干脆一不做二不休,再引入一个硬件吧。于是,I/O 处理机诞生啦,听名字就知道它专门用于处理 IO,并且它其实是一种处理器,只不过用的是另一套擅长 IO 的指令系统,随时可以处理数据。有了 I/O 处理机的帮忙,CPU 甚至可以不知道有传输这回事,这下 CPU 才真正爽到家啦。同样, 这也是需要单独的硬件来支持。
综上所述,硬盘不符合第 1 种方法,因为它需要在某种条件下才能传输。第 4 种和第 5 种需要单独 的硬件支持,先不说我们的 bochs 能否模拟这两种硬件,单独学习这两类硬件的操作方法就很头疼,大家有兴趣的话还是先放一放,以后再琢磨吧。所以在我们的系统中,我们用了第 2、3 这两种(查询传送、中断传送)软件传输方式。
让MBR使用硬盘
我们的MBR从BIOS手中接过了接力棒,但是MBR只有512字节,这么小的空间着实干不了大事情。从MBR接棒的那时候我们就知道还会继续交接的,但是下一棒要交给谁?
接下来在之前的MBR基础上,做一些改进,让它能够读写硬盘!
我们的MBR受限于512字节大小,在那么小的空间中,没法为内核做好准备环境,更没法将内核成功加载到内存并运行。所以要在另一个程序中万层初始化环境及加载内核的任务,这个程序叫 loader ,即加载器。
问题来了,loader在哪里?如果跳过去执行?这就是新改版MBR的使命,简而言之就是负责把loader加载到内存,并将接力棒交给它。
由于MBR占据了硬盘的第0扇区(以LBA逻辑寻址方式,扇区从0开始编号,若以物理CHS方式,扇区则从1开始编号),第一扇区是空闲的可以用,但和MBR离的太近了不是很放心,所以把loader放在第2扇区。MBR从第2扇区中给它读出来。读出来放到哪里呢?原则上是找个空闲地方就行了,在表[[实模式下的内存布局.png]]中查看下,只要在“用途”列中注明“可用区域”的地方都可以用。 0x500~0x7BFF 和 0x7E00~9FBFF 这两段内存区域都可以。
首先,loader中要定义一些数据结构(如[[GDT全局描述符表]],不懂没关系,后面会说),这些数据结构将来的内核还是要继续使用的,所以loader加载到内存后不能被覆盖。
其次,随着咱们不断添加功能,内核必然越来越大,其所在的内存地址也会向越来越高的地方发展,难免会超过可用区域的上限,先尽量把loader放在内存低处,多留出一些空间给内核。 所以,将loader的加载地址选为0x900。为什么不是0x500,这个多省空间?还是预留出一定空间把,彼此隔开远一点心里才踏实,不差这点空间了~这完全是个人偏好,大家随意!
按照上面所说的规划,接下来的代码就是改头换面的新款MBR,代码量增长到126行。
1 | ;主引导程序 |
程序最开始的%include "boot.inc"
,这个%include 是 nasm 编译器中的预处理指令,意思是让编译器在 编译之前把 boot.inc 文件包含进来。任何编译器都应该有 include 之类的能够包含其他文件的预处理指令, 不要认为底层的汇编语言就应该简陋到一穷二白,这和语言是没关系的,是编译器为了开发人员方便管理代码,应该加的。
boot.inc 的内容很简单,目前就两句话,文件内容如下。
1 | ;------------- loader和kernel ---------- |
boot.inc 是我们的配置文件,我们目前关于加载器的配置信息就写在里面,今后还会在此添加更多的配置信息。大家看到的这两句也是预处理命令,是 nasm 提供的宏,和 C 语言中的宏是一回事。只不过 nasm 中的语法是:宏名 equ 值,而 C 语言中的宏是由#define 指令来实现的。所以 LOADER_BASE_ADDR 和 LOADER_START_SECTOR 是两个宏名。
函数名 rd_disk_m_16 的意思是“在 16 位模式下读硬盘”,此函数是咱们本节的重点。
第64行的“mov esi,eax”是把eax中的值先备份到esi中。因为al在out指令中会被用到,这会影 响到eax的低8位。
第 65 行是备份读取的扇区数到 di 寄存器,di 寄存器是 16 位的,和 cx 大小一致。cx 的值会在读取数 据时用到,所以在此提前备份。
第 67~70 行,按照咱们操作硬盘的约定,先选定一个通道,再往 sector count 寄存器中写扇区数。往端口中写入数据用 out 指令,注意 out 指令中 dx 寄存器是用来存储端口号的。先查看咱们 bochs 配置文件关于硬盘的配置部分如下:
1 | # 硬盘设置 |
咱们的虚拟硬盘属于 ata0,是 Primary 通道,所以其 sector count 寄存器是由 0x1f2 端口来访问的。顺便再看第二行的 ata0-master,path=”hd60M.img”,这说明 hd60M.img 是主盘。
第74~95行是将LBA地址写入三个LBA 寄存器和device寄存器的低4位。端口0x1f3是寄存器LBA low,端口 0x1f4 是寄存器 LBA mid,端口 0x1f5 是寄存器 LBA high。shr 指令是逻辑右移指令,这里主要 通过此指令置换出地址的相应部分,写入相应的LBA寄存器。第93行的“or al,0xe0”,用了or“或” 指令和 0xe0 做或运算,拼出 device 寄存器的值。高 4 位为 e,即高 4 位的二进制表示为 1110,其第 5 位 和第 7 位固定为 1,第 6 位为 1 表示启用 LBA。大家可以参考注释。
第 97~100 行便是写入命令啦,因为我们这里是读操作,所以读扇区的命令是 0x20。通过 out 指令写 入 command 端口 0x1f7 后,硬盘就开始工作了。
第 102~109 行检测 status 寄存器的 BSY 位。由于 status 寄存器依然是 0x1f7 端口,所以不需要再为 dx 重新赋值。
105 行的 nop 表示空操作,即什么也不做,只是为了增加延迟,相当于 sleep 了一小下,目 的是减少打扰硬盘的工作。对同一端口在读写两种操作时有不同的用途,在读硬盘时,此端口中的值是硬 盘的工作状态。
第 106 行是将 Status 寄存器的值读入到 al 寄存器,通过第 107 行的 and“与”操作,保留 第 4 位和第 7 位,第 4 位若为 1,表示数据已经准备好,可以传输了。若第 7 位为 1,表示硬盘现在正忙着。只要判断第 4 位是否为 1 就好了,用第 108 行的 cmp 指令和 0x08 做减法运算,判断第 4 位是否为 1。
cmp 指令并不改变操作数的值,只是根据结果去设置标志位,从而咱们根据标志位反着去判断结果。cmp 指令会影响的标志位有 ZF、CF、PF 等,这里咱们借助 ZF 位来判断 cmp 的结果。于是用第 109 行的 jnz .not_ready 来判断结果是否不等于 0,即若等于 0,则 status 寄存器的第 4 位为 1,这表示可以读数据了。 若不等于 0,说明 status 寄存器的第 4 位为 0,表示硬盘正忙(此时 status 寄存器第 7 位肯定为 1)。.not_ready 是个标号,于是跳回去继续判断硬盘状态,直到硬盘把数据准备好才跳出这个循环。
第 111~122 行是从硬盘取数据的过程。由于 data 寄存器是 16 位,即每次 in 操作只读入 2 字节,根据读入的数据总量(扇区数*512 字节)来求得执行 in 指令的次数。这里的乘法用 mul 指令,在实模式下, mul 指令可以做 8 位乘法和 16 位乘法,格式是:mul 操作数。操作数可以是寄存器或内存。乘法运算至少要有两个数参与才行,这里的操作数只是一个乘数,被乘数隐含在 al 或 ax 寄存器中(mul 指令被设计 成这样的,由于历史原因产生很多奇怪的用法,习惯就好啦)。如果操作数是 8 位,被乘数就是 al 寄存器的值,乘积就是 16 位,位于 ax 寄存器。如果操作数是 16 位,被乘数就是 ax 寄存器的值,乘积就是 32 位,积的高 16 位在 dx 寄存器,积的低 16 位在 ax 寄存器。
虽然我们进行的是 16 位的乘法,其结果是 32 位,但由于我知道这两个乘数 ax 的值和 dx 的值都不大,ax 的实际的值其实是 1,乘出来的这个结果,其高位是 0,所以在第 115 行的“mov cx, ax”我们只将这个结果的低 16 位移入 cx 作为循环读取的次数。此处用 8 位乘法不合适,因为 256 超过了 8 位寄存器表示的范围。
在第 118~122 行通过循环来将数据写入 bx 寄存器指向的内存,每读入 2 个字节,bx 所指的地 址便+2。值得注意的是由于在实模式下偏移地址为 16 位,所以用 bx 只会访问到 0~FFFFh 的偏移。待写 入的地址超过 bx 的范围时,从硬盘上读出的数据会把 0x0000~0xffff 的覆盖,所以此处加载的程序不能 超过 64KB,即 2 的 16 次方等于 65536。由于本 mbr 是用来加载 loader 的,所以 loader.bin 要小于 64KB 才行。这一点大可以放心,我们最终的 loader 不超过 2KB,将来的内核也不会超过 70KB。
也许有同学会说,把 bx 改为 ebx 行吗?也不行,在实模式下,CPU 依然会用 16 位偏移地址。这是实 模式下访问内存的规定与缺陷,还记得那个“段基址+段内偏移地址”吗?段内偏移地址正因为是 16 位, 只能访问 64KB 的段空间,所以才将段基址乘以 16 来突破这 64KB,从而实现访问低调 1MB 空间的。
第 123 行返回指令 ret,它用来从函数中返回。如果我们没有定义函数,就不需要它了。函数和一般 代码相比,就是在被调用时,CPU 会将返回地址压到栈中,所以在函数体中,要用 ret 指令将栈中的返回 地址重新加载到程序计数器中,如 cs:ip,这样程序便恢复到之前的执行顺序了。
执行完第 123 行后,程序便回到了第 55 行,这是个跳转的指令。个人觉得,jmp 指令和 call 指令是必不 可少的,jmp 表示一去不回头,call 表示去了还回来。各有各的用途。这里是 MBR 交出接力棒的一刻,采用 jmp 是唯一合适的选择。Jmp 的操作数是 LOADER_BASE_ADDR,即 0x900,这是要跳到内核加载器的节奏。 MBR 到此结束了使命,顺序完成了第二棒的拼接。
接下来的工作是编译,本次的编译较之前相比,多加了一个参数 -I。此参数的意思还是参见 nasm 帮 助,nasm –h 回车,找到-I 的说明。-I<path> adds a pathname to the include file path
大概意思是添加一个包含文件的路径,其实就是添加个库目录。为了目录整洁一些,我在 boot 目录下建立了一个子目录 include,并把 boot.inc 放到了 include 目录下。nasm 要用-I 指定库目录,所以在 boot 目录下输入: nasm -I include/ -o mbr.bin mbr.S 回车。
接下来用 dd 命令将 mbr.bin 写入虚拟硬盘dd if=./mbr.bin of="./hd60M.img" bs=512 count=1 conv=notrunc
实现内核加载器
这一节的内容并不长,因为在进入[[保护模式 | 保护模式]]之前,我们能做的不多,loader 是要经过实模式到保护模式的过渡,并最终在保护模式下加载内核。本节只实现一个简单的 loader,本 loader 只在实模式下工作,等学习了保护模式后,我们再来个真格的。
1 | %include "boot.inc" |
本 loader 程序第 2 行代码用到了 LOADER_BASE_ADDR,所以在第 1 行中把 boot.inc 包含进来了, 其值是 0x900。
编译后,将loader.bin写入硬盘第二个分区,第 0 个扇区是 MBR,第 1 个扇区是空的未使用,原因如前所述,纯粹个人喜好。dd if=./loader.bin of=./hd60M.img bs=512 count=1 seek=2 conv=notrunc
接下来用bochs开始验证,如果程序正确的话,MBR会跳转到loader.bin去运行,屏幕上会显示”2 LOADER”并且闪烁!效果如下图所示。