30天自制操作系统
上QQ阅读APP看书,第一时间看更新

第3天 进入32位模式并导入C语言

1 制作真正的IPL

到昨天为止我们讲到的启动区,虽然也称为IPL(Initial Program Loader,启动程序装载器),但它实质上并没有装载任何程序。而从今天起,我们要真的用它来装载程序了。

把我们的操作系统叫作hello-os很不给劲,干脆改个名字吧。我们就叫它“纸娃娃操作系统”。所谓纸娃娃,意思就是说那是用纸糊起来的,虚有其表,不是真娃娃,就像拍电影时用的岩石等道具,其实都是中间空空的冒牌货。也就是说我们现在要开发的操作系统,只是看上去像操作系统,而其实是个没有内容的纸娃娃,所以大家不用想得太困难,轻轻松松来做就好了。

虽然今后我们要一直称它为“纸娃娃操作系统”,而且在相当长的一段时间里也只是把它当作一种演示程序,但到最后我们肯定能开发出一个像模像样的操作系统,这一点请大家放心。

稍微扯远点,其实仔细想一想,这种虚有其表的“纸娃娃”又何止是操作系统呢。就说CPU吧,其实它根本就不懂什么“数”的概念,只是我们设计了一个电路,只要同时传给它电信号0011和0110,它就能输出结果为1001的电信号,而这种电路我们就称之为加法电路。只有人才会把这个结果解读为3+6=9, CPU只是处理这些电信号。换句话说,虽然CPU根本就不懂什么数字,但却能给出正确的计算结果,这就是笔者所谓的“纸娃娃”。

开发过游戏程序的人就会明白,比如我们和计算机下象棋的时候,可能会觉得计算机水平很高,但实际上计算机对象棋规则一窍不通,仅仅是在执行一个程序而已。就算计算机走出了一着妙棋,也根本不是因为它下手毫不留情啊,或者聪明啊,或者求胜心切什么的,这些都是表象,其实它只是按部就班地执行程序而已。也就是说它本身没有内涵,只有一个唬人的外壳,所以才叫“纸娃娃”。“纸娃娃”太厉害了!……操作系统就算是虚有其表、虚张声势又怎样?没什么不好的,这样就可以了!

■■■■■

那么我们先从简单的程序开始吧。因为磁盘最初的512字节是启动区,所以要装载下一个512字节的内容。我们来修改一下程序。改好的程序就是projects/03_day下的harib00aharib是日语中haribote(纸娃娃)的前面几个字母。——译者注,像以前一样,我们把它复制到tolset里来。

这次添加的内容大致如下。

本次添加的部分

MOV      AX,0x0820
MOV      ES, AX
MOV      CH,0              ; 柱面0
MOV      DH,0              ; 磁头0
MOV      CL,2              ; 扇区2

MOV      AH,0x02          ; AH=0x02 : 读盘
MOV      AL,1              ; 1个扇区
MOV      BX,0
MOV      DL,0x00          ; A驱动器
INT      0x13              ; 调用磁盘BIOS
JC       error

新出现的指令只有JC。真好,讲起来也轻松了。所谓JC,是“jump if carry”的缩写,意思是如果进位标志(carry flag)是1的话,就跳转。这里突然冒出来“进位标志”这么个新词,不过大家不用担心,很快就会明白的。

■■■■■

至于“INT 0x13”这个指令,我们虽然知道这是要调用BIOS的0x13号函数,但还不明白它到底是干什么用的,那就来查一下吧。

我们可以找到如下的内容:

❏ 磁盘读、写,扇区校验(verify),以及寻道(seek)

AH=0x02;(读盘)

AH=0x03;(写盘)

AH=0x04;(校验)

AH=0x0c;(寻道)

AL=处理对象的扇区数;(只能同时处理连续的扇区)

CH=柱面号 &0xff;

CL=扇区号(0-5位)|(柱面号&0x300)>>2;

DH=磁头号;

DL=驱动器号;

ES:BX=缓冲地址;(校验及寻道时不使用)

返回值:

FLACS.CF==0:没有错误,AH==0

FLAGS.CF==1:有错误,错误号码存入AH内(与重置(reset)功能一样)

我们这次用的是AH=0x02,哦,原来是“读盘”的意思。

■■■■■

返回值那一栏里的FLAGS.CF又是什么意思呢?这就是我们刚才讲到的进位标志。也就是说,调用这个函数之后,如果没有错,进位标志就是0;如果有错,进位标志就是1。这样我们就能明白刚才为什么要用JC指令了。

进位标志是一个只能存储1位信息的寄存器,除此之外,CPU还有其他几个只有1位的寄存器。像这种1位寄存器我们称之为标志。标志在英文中为flag,是旗帜的意思。标志之所以叫flag是因为它的开和关就像升旗降旗的状态一样。

所谓进位标志,本来是用来表示有没有进位(carry)的,但在CPU的标志中,它是最简单易用的,所以在其他地方也经常用到。这次就是用来报告BIOS函数调用是否有错的。

其他几个寄存器我们也来依次看一下吧。CH、CL、DH、DL分别是柱面号、扇区号、磁头号、驱动器号,一定不要搞错。在上面的程序中,柱面号是0,磁头号是0,扇区号是2,磁盘号是0。

■■■■■

在有多个软盘驱动器的时候,用磁盘驱动器号来指定从哪个驱动器的软盘上读取数据。现在的电脑,基本都只有1个软盘驱动器,而以前一般都是2个。既然现在只有一个,那不用多想,指定0号就行了。

知道了从哪个软盘驱动器读取数据之后,我们接着看从那个软盘的什么地方来读取数据。

如果手头有不用的软盘,希望大家能把它拆开看看。拆开后可以看到,中间有一个8厘米的黑色圆盘,那是一层薄薄的磁性胶片。从外向内,一圈一圈圆环状的区域,分别称为柱面0,柱面1, ……,柱面79。一共有80个柱面。这并不是说工厂就是这样一圈一圈地生产软盘的,只是我们将它作为一个数据存储媒体,是这样组织它的数据存储方式的。柱面在英文中是cylinder,原意是圆筒。磁盘的柱面,尽管高度非常低,但我们可以把它看成是一个套一个的同心圆筒,它正是因此得名的。

下面讲一下磁头。磁头是个针状的磁性设备,既可以从软盘正面接触磁盘,也可以从软盘背面接触磁盘。与光盘不同,软盘磁盘是两面都能记录数据的。因此我们有正面和反面两个磁头,分别是磁头0号和磁头1号。

最后我们看一下扇区。指定了柱面和磁头后,在磁盘的这个圆环上,还能记录很多位信息,按照整个圆环为单位读写的话,实在有点多,所以我们又把这个圆环均等地分成了几份。软盘分为18份,每一份称为一个扇区。一个圆环有18个扇区,分别称为扇区1、扇区2、……扇区18。扇区在英文中是sector,意思是指领域、扇形。

综上所述,1张软盘有80个柱面,2个磁头,18个扇区,且一个扇区有512字节。所以,一张软盘的容量是:

80×2×18×512=1474 560 Byte=1440KB

含有IPL的启动区,位于C0-H0-S1(柱面0,磁头0,扇区1的缩写),下一个扇区是C0-H0-S2。这次我们想要装载的就是这个扇区。

■■■■■

剩下的大家还不明白的就是缓冲区地址了吧。这是个内存地址,表明我们要把从软盘上读出的数据装载到内存的哪个位置。一般说来,如果能用一个寄存器来表示内存地址的话,当然会很方便,但一个BX只能表示0~0xffff的值,也就是只有0~65535,最大才64K。大家的电脑起码也都有64M内存,或者更多,只用一个寄存器来表示内存地址的话,就只能用64K的内存,这太可惜了。

于是为了解决这个问题,就增加了一个叫EBX的寄存器,这样就能处理4G内存了。这是CPU能处理的最大内存量,没有任何问题。但EBX的导入是很久以后的事情,在设计BIOS的时代,CPU甚至还没有32位寄存器,所以当时只好设计了一个起辅助作用的段寄存器(segment register)。在指定内存地址的时候,可以使用这个段寄存器。

我们使用段寄存器时,以ES:BX这种方式来表示地址,写成“MOV AL, [ES:BX]”,它代表ES×16+BX的内存地址。我们可以把它理解成先用ES寄存器指定一个大致的地址,然后再用BX来指定其中一个具体地址。

这样如果在ES里代入0xffff,在BX里也代入0xffff,就是1114 095字节,也就是说可以指定1M以内的内存地址了。虽然这也还是远远不到64M,但当时英特尔公司的大叔们,好像觉得这就足够了。在最初设计BIOS的时代,这种配置已经很能满足当时的需要了,所以我们现在也还是要遵从这一规则。因此,大家就先忍耐一下这1MB内存的限制吧。

这次,我们指定了ES=0x0820, BX=0,所以软盘的数据将被装载到内存中0x8200到0x83ff的地方。可能有人会想,怎么也不弄个整点的数,比如0x8000什么的,那多好。但0x8000~0x81ff这512字节是留给启动区的,要将启动区的内容读到那里,所以就这样吧。

那为什么使用0x8000以后的内存呢?这倒也没什么特别的理由,只是因为从内存分布图上看,这一块领域没人使用,于是笔者就决定将我们的“纸娃娃操作系统”装载到这一区域。0x7c00~0x7dff用于启动区,0x7e00以后直到0x9fbff为止的区域都没有特别的用途,操作系统可以随便使用。

■■■■■

到目前为止我们开发的程序完全没有考虑段寄存器,但事实上,不管我们要指定内存的什么地址,都必须同时指定段寄存器,这是规定。一般如果省略的话就会把“DS:”作为默认的段寄存器。

以前我们用的“MOV CX, [1234]”,其实是“MOV CX, [DS:1234]”的意思。“MOV AL, [SI]”,也就是“MOV AL, [DS:SI]”的意思。在汇编语言中,如果每回都这样写就太麻烦了,所以可以省略默认的段寄存器DS。

因为有这样的规则,所以DS必须预先指定为0,否则地址的值就要加上这个数的16倍,就会读写到其他的地方,引起混乱。

好,我们来执行这个程序看看吧。如果程序有什么错,它就会显示错误信息。但估计不会出什么问题吧。没错的话,它就什么都不做(笑)。所以,如果屏幕上不显示任何错误信息的话,我们就成功了。

哎呀,有件事差点忘了,Makefile中可以使用简单的变量,于是笔者用变量改写了这次的Makefile。怎么样,是不是比之前稍微容易理解一些?