30天自制操作系统

启动区

(boot sector)软盘第一个的扇区称为启动区。那么什么是扇区呢?计算机读写软
盘的时候,并不是一个字节一个字节地读写的,而是以512字节为一个单位进行读
写。因此,软盘的512字节就称为一个扇区。一张软盘的空间共有1440KB,也就是
1474560字节,除以512得2880,这也就是说一张软盘共有2880个扇区。那为什么
第一个扇区称为启动区呢?那是因为计算机首先从最初一个扇区开始读软盘,然
后去检查这个扇区最后2个字节的内容。
如果这最后2个字节不是0x55 AA,计算机会认为这张盘上没有所需的启动程序,就
会报一个不能启动的错误。(也许有人会问为什么一定是0x55 AA呢?那是当初的
设计者随便定的,笔者也没法解释)。如果计算机确认了第一个扇区的最后两个字
节正好是0x55 AA,那它就认为这个扇区的开头是启动程序,并开始执行这个程序。

IPL

initial program loader的缩写。启动程序加载器。启动区只有区区512字节,实际的
操作系统不像hello-os这么小,根本装不进去。所以几乎所有的操作系统,都是把
加载操作系统本身的程序放在启动区里的。有鉴于此,有时也将启动区称为IPL。 但hello-os没有加载程序的功能,所以HELLOIPL这个名字不太顺理成章。如果有
人正义感特别强,觉得“这是撒谎造假,万万不能容忍!”,那也可以改成其他的
名字。但是必须起一个8字节的名字,如果名字长度不到8字节的话,需要在最后
补上空格。

启动

(boot)boot这个词本是长靴(boots)的单数形式。它与计算机的启动有什么关系
呢?一般应该将启动称为start的。实际上,boot这个词是bootstrap的缩写,原指靴
子上附带的便于拿取的靴带。但自从有了《吹牛大王历险记》(德国)这个故事
以后,bootstrap这个词就有了“自力更生完成任务”这种意思(大家如果对详情感
兴趣,可以在Google上查找,也可以在帮助和支持网页http://hrb.osask.jp上提问)。
而且,磁盘上明明装有操作系统,还要说读入操作系统的程序(即IPL)也放在磁
盘里,这就像打开宝物箱的钥匙就在宝物箱里一样,是一种矛盾的说法。这种矛
盾的操作系统自动启动机制,被称为bootstrap方式。boot这个说法就来源于此。如
果是笔者来命名的话,肯定不会用bootstrap 这么奇怪的名字,笔者大概会叫它“多
级火箭式”吧。

寄存器

AX——accumulator,累加寄存器
CX——counter,计数寄存器
DX——data,数据寄存器
BX——base,基址寄存器
SP——stack pointer,栈指针寄存器
BP——base pointer,基址指针寄存器
SI——source index,源变址寄存器
DI——destination index,目的变址寄存器

大家所用的电脑里配置的,大概都是64MB,甚至512MB这样非常大的内存。那是不是这些
内存我们想怎么用就能怎么用呢?也不是这样的。比如说,内存的0号地址,也就是最开始的部
分,是BIOS程序用来实现各种不同功能的地方,如果我们随便使用的话,就会与BIOS发生冲突,
结果不只是BIOS会出错,而且我们的程序也肯定会问题百出。另外,在内存的0xf0000号地址附
近,还存放着BIOS程序本身,那里我们也不能使用。

0x00007c00-0x00007dff :启动区内容的装载地址

看到这,大家可能会问:“为什么是0x7c00呢? 0x7000不是更简单、好记吗?”其实笔者也
是这么想的,不过没办法,当初规定的就是0x7c00。做出这个规定的应该是IBM的大叔们,不过
估计他们现在都成爷爷了。

通过以上的尝试,最终证明,不管是CPU还是内存,它们根本就不关心所处理的电信号
到底代表什么意思。这么一来,说不定我们拿数码相机拍一幅风景照,把它作为磁盘映像文
件保存到磁盘里,就能成为世界上最优秀的操作系统!这看似荒谬的情况也是有可能发生的。
但从常识来看,这样做成的东西肯定会故障百出。反之,我们把做出的可执行文件作为一幅
画来看,也没准能成为世界上最高水准的艺术品。不过可以想象的是,要么文件格式有错,
要么显示出来的图是乱七八糟的。

INT 13

 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
 

说明

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)功能一样)

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

上面虽然写着486用,但并不是说会出现仅能在486中执行的机器语言,这只是单纯的词语解
释的问题。所以486用的模式下,如果只使用16位寄存器,也能成为在8086中亦可执行的机器语言。
“纸娃娃操作系统”也支持386,所以虽然这里指定的是486,但并不是386中就不能用。可能会有
人问,这里的386,486都是什么意思啊?我们来简单介绍一下电脑的CPU(英特尔系列)家谱

8086→80186→286→386→486→Pentium→PentiumPro→PentiumII→PentiumIII→Pentium4→…

中断

首先是CLI和STI。所谓CLI,是将中断标志(interrupt flag)置为0的指令(clear interrupt flag)。
STI是要将这个中断标志置为1的指令(set interrupt flag)。而标志,是指像以前曾出现过的进位标
志一样的各种标志,也就是说在CPU中有多种多样的标志。更改中断标志有什么好处呢?正如其
名所示,它与CPU的中断处理有关系。当CPU遇到中断请求时,是立即处理中断请求(中断标志为1),还是忽略中断请求(中断标志为0),就由这个中断标志位来设定。

image.png

全局段号记录表

GDT是“global(segment)descriptor table”的缩写,意思是全局段号记录表。将这些数据整
齐地排列在内存的某个地方,然后将内存的起始地址和有效设定个数放在CPU内被称作GDTR①的
特殊寄存器中,设定就完成了。

中断记录表

IDT是“interrupt descriptor table”的缩写,直译过来就是“中断记录表”。当CPU遇到
外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理这种突发事件。这就是中
断功能。

我们拿电脑的键盘来举个例子。以CPU的速度来看,键盘特别慢,只是偶尔动一动。就算是
重复按同一个键,一秒钟也很难输入50个字符。而CPU在1/50秒的时间内,能执行200万条指令
(CPU主频100MHz时)。CPU每执行200万条指令,查询一次键盘的状况就已经足够了。如果查询
得太慢,用户输入一个字符时电脑就会半天没反应。

要是设备只有键盘,用“查询”这种处理方法还好。但事实上还有鼠标、软驱、硬盘、光驱、
网卡、声卡等很多需要定期查看状态的设备。其中,网卡还需要CPU快速响应。响应不及时的话,
数据就可能接受失败,而不得不再传送一次。如果因为害怕处理不及时而靠查询的方法轮流查看
各个设备状态的话,CPU就会穷于应付,不能完成正常的处理

正是为解决以上问题,才有了中断机制。各个设备有变化时就产生中断,中断发生后,CPU
暂时停止正在处理的任务,并做好接下来能够继续处理的准备,转而执行中断程序。中断程序执
行完以后,再调用事先设定好的函数,返回处理中的任务。正是得益于中断机制,CPU可以不用
一直查询键盘,鼠标,网卡等设备的状态,将精力集中在处理任务上。

PIC

所谓PIC是“programmable interrupt controller”的缩写,意思是“可编程中断控制器”。PIC
与中断的关系可是很密切的哟。它到底是什么呢?在设计上,CPU单独只能处理一个中断,这不
够用,所以IBM的大叔们在设计电脑时,就在主板上增设了几个辅助芯片。现如今它们已经被集
成在一个芯片组里了。

PIC是将8个中断信号①集合成一个中断信号的装置。PIC监视着输入管脚的8个中断信号,只
要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知给CPU。IBM的大叔们想要
通过增加PIC来处理更多的中断信号,他们认为电脑会有8个以上的外部设备,所以就把中断信号
设计成了15个,并为此增设了2个PIC。

与CPU直接相连的PIC称为主PIC(master PIC),与主PIC相连的PIC称为从PIC(slave PIC)。
主PIC负责处理第0到第7号中断信号,从PIC负责处理第8到第15号中断信号。master意为主人,
slave意为奴隶,笔者搞不清楚这两个词的由来,但现在结果是不论从PIC如何地拼命努力,如果
主PIC不通知给CPU,从PIC的意思也就不能传达给CPU。或许是从这种关系上考虑,而把它们一
个称为主人,一个称为奴隶。

image.png

中断屏蔽寄存器

现在简单介绍一下PIC的寄存器。首先,它们都是8位寄存器。IMR是“interrupt mask register”
的缩写,意思是“中断屏蔽寄存器”。8位分别对应8路IRQ信号。如果某一位的值是1,则该位所
对应的IRQ信号被屏蔽,PIC就忽视该路信号。这主要是因为,正在对中断设定进行更改时,如
果再接受别的中断会引起混乱,为了防止这种情况的发生,就必须屏蔽中断。还有,如果某个IRQ
没有连接任何设备的话,静电干扰等也可能会引起反应,导致操作系统混乱,所以也要屏蔽掉这
类干扰。

ICW是“initial control word”的缩写,意为“初始化控制数据”。因为这里写着word,所以
我们会想,“是不是16位”?不过,只有在电脑的CPU里,word这个词才是16位的意思,在别的
设备上,有时指8位,有时也会指32位。PIC不是仅为电脑的CPU而设计的控制芯片,其他种类的
CPU也能使用,所以这里word的意思也并不是我们觉得理所当然的16位。

ICW有4个,分别编号为1~4,共有4个字节的数据。ICW1和ICW4与PIC主板配线方式、中断
信号的电气特性等有关,所以就不详细说明了。电脑上设定的是上述程序所示的固定值,不会设
定其他的值。如果故意改成别的什么值的话,早期的电脑说不定会烧断保险丝,或者器件冒
烟①;最近的电脑,对这种设定起反应的电路本身被省略了,所以不会有任何反应。

ICW3是有关主—从连接的设定,对主PIC而言,第几号IRQ与从PIC相连,是用8位来设定的。
如果把这些位全部设为1,那么主PIC就能驱动8个从PIC(那样的话,最大就可能有64个IRQ),
但我们所用的电脑并不是这样的,所以就设定成00000100。另外,对从PIC来说,该从PIC与主PIC
的第几号相连,用3位来设定。因为硬件上已经不可能更改了,如果软件上设定不一致的话,只
会发生错误,所以只能维持现有设定不变。

因此不同的操作系统可以进行独特设定的就只有ICW2了。这个ICW2,决定了IRQ以哪一号
中断通知CPU。“哎?怎么有这种事?刚才不是说中断信号的管脚只有1根吗?”嗯,话是那么说,
但PIC还有个挺有意思的小窍门,利用它就可以由PIC来设定中断号了

大家可能会对此有兴趣,所以再详细介绍一下。中断发生以后,如果CPU可以受理这个
中断,CPU就会命令PIC发送2个字节的数据。这2个字节是怎么传送的呢?CPU与PIC用IN
或OUT进行数据传送时,有数据信号线连在一起。PIC就是利用这个信号线发送这2个字节数
据的。送过来的数据是“0xcd 0x??”这两个字节。由于电路设计的原因,这两个字节的数据
在CPU看来,与从内存读进来的程序是完全一样的,所以CPU就把送过来的“0xcd 0x??”作
为机器语言执行。这恰恰就是把数据当作程序来执行的情况。这里的0xcd就是调用BIOS时
使用的那个INT指令。我们在程序里写的“INT 0x10”,最后就被编译成了“0xcd 0x10”。所
以,CPU上了PIC的当,按照PIC所希望的中断号执行了INT指令

下面要讲的内容可能有点偏离主题,但笔者还是想介绍一下“纸娃娃系统”的内存分布图。
0x00000000 - 0x000fffff : 虽然在启动中会多次使用,但之后就变空。(1MB)
0x00100000 - 0x00267fff : 用于保存软盘的内容。(1440KB)
0x00268000 - 0x0026f7ff : 空(30KB)
0x0026f800 - 0x0026ffff : IDT (2KB)
0x00270000 - 0x0027ffff : GDT (64KB)
0x00280000 - 0x002fffff : bootpack.hrb(512KB)
0x00300000 - 0x003fffff : 栈及其他(1MB)
0x00400000 - : 空
这个内存分布图当然是笔者所做出来的。为什么要做成这呢?其实也没有什么特别的理由,
觉得这样还行,跟着感觉走就决定了。另外,虽然没有明写,但在最初的1MB范围内,还有BIOS,
VRAM等内容,也就是说并不是1MB全都空着。

禁止高速缓存

为了禁止缓存,需要对CR0寄存器的某一标志位进行操作。对哪里操作,怎么操作,大家一
看程序就能明白。这时,需要用到函数load_cr0和store_cr0,与之前的情况一样,这两个函数不
能用C语言写,只能用汇编语言来写,存在naskfunc.nas里。

可编程的间隔型定时器

要在电脑中管理定时器,只需对PIT进行设定就可以了。PIT是“ Programmable Interval Timer”
的缩写,翻译过来就是“可编程的间隔型定时器”。我们可以通过设定PIT,让定时器每隔多少秒
就产生一次中断。因为在电脑中PIT连接着IRQ(interrupt request,参考第6章)的0号,所以只要
设定了PIT就可以设定IRQ0的中断间隔。……在旧机种上PIT是作为一个独立的芯片安装在主板
上的,而现在已经和PIC(programmable interrupt controller,参考第6章)一样被集成到别的芯片
里了。

分辨率

其实说起来也很简单。给AX赋值0x4f02,给BX赋值画面模式号码,这样就可以切换到高分
辨率画面模式了。为什么呢?这个笔者也答不上来,原本就是这样的。这次我们只是正好使用到
了这个功能。以前画面是320×200的时候,我们用的是“AH=0; AL=画面模式号码;”。现在切换
到新画面时就使用“AX = 0x4f02;”。

有鉴于此,多家显卡公司经过协商,成立了VESA协会(Video Electronics Standards
Association,视频电子标准协会)。此后,这个协会制定了虽然不能说完全兼容、但几乎可以通
用的设定方法,制作了专用的BIOS。这个追加的BIOS被称作“VESA BIOS extension”(VESA-BIOS
扩展,简略为VBE)。利用它,就可以使用显卡的高分辨率功能了。