第1章 马上动手写一个最小的“操作系统”
勿以善小而不为.
——刘备
虽说万事开头难,但有时也未必.比如说,写一个有实用价值的操作系统是一项艰
巨的工作,但一个最小的操作系统或许很容易就实现了.现在我们就来实现一个小得无
法再小的“操作系统”,建议你跟随书中的介绍一起动手来做,你会发现不但很容易,而
且很有趣.
1.1 准备工作
对于写程序,准备工作无非就是硬件和软件两方面,我们来看一下:
1.硬件
~ 一台计算机(Windows 操作系统)
~ 一张空白软盘
2.软件
~ 汇编编译器NASM.最新版本可以在此链接处获得:
http://sourceforge.net/projects/nasm.
(此刻你可能会有疑问:为什么是NASM,而不是MASM 或者
TASM?对于这一点本书后面会有解释.)
~ 软盘绝对扇区读写工具.比如本书附赠光盘中的
FloppyWriter.exe .
1.2 10 分钟完成的操作系统
你相不相信,一个“操作系统”可以只有20 行代码?请看:
代码1-1 \chapter1\a\boot.asm
-------------------------------------------------------------
org 07c00h ; 告诉编译器程序加载到7c00 处
mov ax, cs
mov ds, ax
mov es, ax
call DispStr ; 调用显示字符串例程
jmp $ ; 无限循环
DispStr:
mov ax, BootMessage
mov bp, ax ; es:bp = 串地址
mov cx, 16 ; cx = 串长度
mov ax, 01301h ; ah = 13, al = 01h
mov bx, 000ch ; 页号为0(bh = 0) 黑底红字(bl = 0Ch,高亮)
mov dl, 0
int 10h ; 10h 号中断
ret
BootMessage: db "Hello, OS world!"
times 510-($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为
; 512 字节
dw 0xaa55 ; 结束标志
-------------------------------------------------------------
把这段代码用NASM 编译一下:
nasm boot.asm –o boot.bin
我们就得到了一个512B 的boot.bin,使用软盘绝对扇区读写工具将这个文件写到一
张空白软盘的第一个扇区.好了,你的第一个“操作系统”就已经完成了.这张软盘已
经是一张引导盘了.
把它放到你的软驱中重新启动计算机,从软盘引导,你看到了什么?
计算机显示出你的字符串了!红色的“Hello, OS world!”,多么奇妙啊,你的“操作
系统”在运行了!
如果使用Virtual PC 的话(下文中将会有关于Virtual PC 的详细介绍),你应该能看
到图1-1 所示的画面.
这真的是太棒了,虽然你知道它有多么简陋,但是,毕竟你已经制作了一个可以引
导的软盘了,而且所有工作都是你亲手独立完成的!
图1-1 最小的 "操作系统"
1.3 Boot Sector
你可能还没有从刚刚的兴奋中走出来,可是我不得不告诉你,实际上,你刚刚所完
成的并不是一个完整的OS,而仅仅是一个最最简单的引导扇区(Boot Sector).然而不
管我们完成的是什么,至少,它是直接在裸机上运行的,不依赖于任何其他软件,所以,
这和我们平时所编写的应用软件有本质的区别.它不是操作系统,但已经具备了操作系
统的一个特性.
我们知道,当计算机电源被打开时,它会先进行加电自检(POST),然后寻找启动
盘,如果是选择从软盘启动,计算机就会检查软盘的0 面0 磁道1 扇区,如果发现它以
0xAA55(假如我们把此扇区看做一个字符数组sector[]的话,那么此结束标志相当于
sector[510]=0x55,且sector[511]=0xAA)结束,则BIOS 认为它是一个引导扇区,也就是
我们说的Boot Sector.当然,一个正确的Boot Sector 除了以0xAA55 结束之外,还应该
包含一段少于512B 的执行码.
好了,一旦BIOS 发现了Boot Sector,就会将这512B 的内容装载到内存的0000:7c00
处,然后跳转到0000:7c00 处将控制权彻底交给这段引导代码.到此为止,计算机不再由
BIOS 中固有的程序来控制,而变成由操作系统的一部分来控制.
现在,你可能明白了为什么在那段代码的第一行会出现org 07c00 这样的代码.没错,
这行代码就是告诉编译器,将来我们的这段程序要被加载到内存偏移地址7c00 处.好了,
下面将对代码的其他部分进行详细解释.
1.4 代码解释
其实程序的主体框架只有5 行(从第2 行到第6 行),其中调用了一个显示字符串的
子程序.程序的第2、3、4 行是3 个mov 指令,使ds 和es 两个段寄存器指向与cs 相同
的段,以便在以后进行数据操作的时候能定位到正确的位置.第5 行调用子程序显示字
符串,然后jmp $让程序无限循环下去.
可能大部分人开始学汇编时用的都是MASM,其实NASM 的格式跟MASM 总体上
是差不多的,在这段程序中,值得说明的地方有以下几点:
(1)在NASM 中,任何不被方括号[]括起来的标签或变量名都被认为是地址,访问
标签中的内容必须使用[].所以,
mov ax, BootMessage
将会把“Hello, OS world!”这个字符串的首地址传给寄存器ax.又比如,如果有:
foo dw 1
则mov ax, foo 将把foo 的地址传给ax,而mov bx, [foo]将把bx 的值赋为1.
实际上,在NASM 中,变量和标签是一样的,也就是说:
foo dw 1 ≡ foo: dw 1
而且你会发现,Offset 这个关键字在NASM 也是不需要的.因为不加方括号时表示
的就是Offset.
笔者认为这是NASM的一大优点,要地址就不加方括号,也不必额外地用什么Offset,
想要访问地址中的内容就必须加上方括号.代码规则非常鲜明,一目了然.
(2)关于$和$$.$表示当前行被汇编后的地址.这好像不太好理解,不要紧,我们
把刚刚生成的二进制代码文件反汇编来看看:
ndisasmw -o 0x7c00 boot.bin >> disboot.asm
打开disboot.asm,你会发现这样一行:
00007C09 EBFE jmp short 0x7c09
明白了吧,$在这里的意思原来就是0x7c09.
那么$$表示什么呢?它表示一个节(section)的开始处被汇编后的地址.在这里,
我们的程序只有1 个节,所以,$$实际上就表示程序被编译后的开始地址,也就是0x7c00.
1
2
注意:这里的section 属于NASM 规范的一部分,表示一段代码,关于它和
$$更详细的注解请参考NASM 联机技术文档.
在写程序的过程中,$-$$可能会被经常用到,它表示本行距离程序开始处的相对距
离.现在,你应该明白510-($-$$)表示什么意思了吧?times 510-($-$$) db 0 表示将0 这个
字节重复510-($-$$)遍,也就是在剩下的空间中不停地填充0,直到程序有510B 为止.
这样,加上结束标志0xAA55 占用的2B,恰好是512B.
1.5 水面下的冰山
即便是非常袖珍的程序,也有可能遇到不能正确运行的情况,对此你一定并不惊讶,
谁都可能少写一个标点,或者在一个小小的逻辑问题上犯迷糊.好在我们可以调试,通
过调试,可以发现错误,让程序日臻完美.但是对于操作系统这样的特殊程序,我们没
有办法用普通的调试工具来调试.可是,哪怕一个小小的Boot Sector,我们也没有十足
的把握一次就写好,那么,遇到不能正确运行的时候该怎么办呢?在屏幕上没有看到我
们所要的东西,甚至于机器一下子重启了,你该如何是好呢?
每一个问题都是一把锁,你要相信,世界上一定存在一把钥匙可以打开这把锁.你
也一定能找到这把钥匙.
一个引导扇区代码可能只有20 行,如果Copy&Paste 的话,10 秒钟就搞定了,即便
自己敲键盘抄一遍下来,也用不了10 分钟.可是,在遇到一个问题时,如果不小心犯了
小错,自己到运行时才发现,你可能不得不花费10 个10 分钟甚至更长时间来解决它.
笔者把这20 行的程序称做水面以上的冰山,而把你花了数小时的时间做的工作称做水面
下的冰山.
古人云:“授之以鱼,不如授之以渔.”本书将努力将冰山下的部分展示给读者.这
些都是笔者经历了痛苦的摸索后的一些心得,这些方法可能不是最好的,但至少可以给
你提供一个参考.
好了,以Boot Sector 为例,你可以想像得到,将来我们一定会对这20 行进行扩充,
最后得到200 行甚至更多的代码,我们总得想一个办法,让它调试起来容易一些.其实
很容易,我们只要把“org 07c00h”这一行改成“org 0100h”就可以编译成一个.COM
文件让它在DOS 下运行了.我们来试一试,首先把07c00h 改成0100h,编译:
nasm boot.asm –o boot.com
好了,一个易于执行和调试的Boot Sector 就制作完毕了.调试.COM 文件可能让你
仿佛一下子回到了20 世纪,没关系,怀旧一下感觉还是蛮不错的.
Turbo Debugger 是一个不错的调试工具,“图形化”界面,全屏操作,用起来感觉和
一个“严重”精简后的Windows 差不多☺.
调试完之后要放到软盘上进行试验,我们需要再把0100h 改成07c00h,这样改来改
去比较麻烦,好在强大的NASM 给我们提供了预编译宏,把原来的“org 07c00h”一行
变成许多行即可:
代码1-2 节自\chapter1\b\boot.asm
-------------------------------------------------------------------
;%define _BOOT_DEBUG_ ; 做 Boot Sector 时一定将此行注释掉!
; 将此行打开后用 nasm Boot.asm -o Boot.com
; 做成一个.COM 文件易于调试
%ifdef _BOOT_DEBUG_
org 0100h ; 调试状态,做成 .COM 文件,可调试
%else
org 07c00h ; Boot 状态,BIOS 将把 Boot Sector 加载到 0:7C00
; 处并开始执行
%endif
-------------------------------------------------------------------
这样一来,如果我们想要调试,就让第一行有效,想要做Boot Sector 时,将它注释
掉就可以了.
这里的预编译命令跟C 差不多,就不用多解释了.
至此,你不但已经学会了如何写一个简单的引导扇区,更具备了扩充它的条件——
知道如何排错,如何调试.这就好比从石器时代走到了铁器时代,宽阔的道路展现在眼
前,运用工具,你无所不能.
如果说开始我们还是在黑暗中摸索,到现在为止,我们至少已经看到了光亮,我们
有信心将Boot Sector 进行不断扩充,让它变成一个真正的操作系统的一部分.
1.6 回顾
让我们再回过头看看刚才那段代码吧,大部分代码你一定已经读懂了.如果你还是
一个NASM 新手,可能并不是对所有的细节都那么清晰.但是,毕竟你已经发现,原来
可以如此容易地迈出写操作系统的第一步.
是啊,这是个并不十分困难的开头,如果你也这样认为,就请带上百倍的信心,以
及一直以来想要探索OS 奥秘的热情,随我一起出发吧!
上传的附件: