写一个操作系统(〇)——最简单的系统

引言

  最近在学操作系统,所以就想着自己实现一个操作系统顺便写一系列教程。而这篇博客就是第一篇。虽然说是一系列教程,然而未必就能够坚持到底,所以,这篇也许是最后一篇。

一个最简单的操作系统

操作系统是怎样运行起来的

  要想写一个操作系统,首先得明白操作系统都干了些什么。但这里既然是讲一个最简单的操作系统,当然是什么都不干的操作系统最简单了。可是如果真的什么都不干,那就等于不存在了。所以还是得干一点最简单的事,比如输出个”Hello, memi OS!”。
  我们的操作系统要想输出个”Hello, memi OS!”,首先得运行起来。那么操作系统是怎么运行起来的呢?
  当我们按下计算机的电源键后,计算机就会自动开始执行被固化到计算机ROM芯片上的BIOS程序,这个程序在完成自检等操作后,就会去读取磁盘物理起始位置的扇区(512Byte)加载到内存中(当然,前提是这个扇区已被申明为引导扇区),然后自动跳转到这个块数据在内存中的位置并开始执行这段指令。如果你自己安装过操作系统或者接触过BIOS,你就会知道计算机启动时是可以在BIOS里选择启动磁盘的,其实这里选择启动盘,就是在选择BIOS加载哪一个磁盘的起始扇区。所以这里我们就利用这个块来载入我们的操作系统。

编写最简单的操作系统

申明引导扇区

  为了载入我们的最简单的操作系统,我们就要把磁盘(或者软盘)中的第一个扇区申明为一个引导扇区(主引导记录MBR),申明引导扇区的方法很简单,就是在起始扇区末尾两个字节写入0xAA55,所以,我们的引导程序可能是这样的:

1
2
3
;boot programs
;510 byte
dw 0xaa55 ; 结束标志

有了这行汇编代码,当我们的程序写入磁盘第一个扇区后,这个扇区就会被识别为引导扇区,BIOS就会加载这个扇区到内存特定位置中并执行它。

“Hello, memi OS!”

  为了输出”Hello, memi OS!”字符串,我们需要在程序中定义一个字符串:

1
BootMessage: db "Hello, memi OS!"

  有了字符串以后,我们需要调用BIOS中断函数将这个字符串显示出来。要知道,我们是在开发一个系统底层最开始的程序,这个时候是没有如同我们在编写普通C程序时的“printf()”这样的系统调用的,那怎样才能显示我们的字符串呢?好在硬件开发商已经在BIOS中为我们初始化了一些最基本的和硬件打交道的接口,这些接口的调用地址被初始化在BIOS的中断向量表(interrupt vector table,IVT)中(如果你对中断还不了解的话,有必要先去了解一下有关中断的知识。)查询x86架构BIOS的中断向量表,可以看到,有一个用于视频服务的中断向量10h,其详细调用方法如下:图片挂掉了!!!调用这个中断,并传入合适的参数,即可显示字符串:

1
2
3
4
5
6
7
8
9
DispStr:
mov ax, BootMessage ; 将前面申明的字符串地址传给ax寄存器
mov bp, ax ; ES:BP = 串地址
mov cx, 15 ; CX = 串长度
mov ax, 01301h ; 显示字符串(AH = 13), 光标跟随(AL = 01h)
mov bx, 000ch ; 页号为0 (BH = 0), 黑底红字(BL = 0Ch,高亮)
mov dl, 0
int 10h ; 10h 号中断
ret

载入引导扇区并显示字符串

  在x86架构中,BIOS会将引导扇区加载到内存中0x7c00h处,并跳转到这里开始执行。为了我们的程序能正确寻址,程序开头必须加上这样一句伪指令:

1
org 0x7c00h ; 告诉编译器程序加载到7c00处

接下来,就是调用显示函数然后进入死循环以便我们可以看见显示的内容了:

1
2
3
4
5
mov ax, cs
mov ds, ax
mov es, ax
call DispStr ; 调用显示字符串例程
jmp $ ; 无限循环(在nasm汇编语言中,$表示当前指令地址)

完整程序

  前面我们说过,一个扇区有512Byte,但如果我们把上面这些程序编译到一起,显然不足512Byte,为了解决这一问题,我们需要在程序后面的空间中都补上0来占位:

1
times 510 - ($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节(在nasm中,$$表示程序起始地址)

所以,我们的整个程序可以是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;filename: boot.asm
org 07c00h ; 告诉编译器程序加载到7c00处
mov ax, cs
mov ds, ax
mov es, ax
call DispStr ; 调用显示字符串例程
jmp $ ; 无限循环
DispStr:
mov ax, BootMessage ; 将前面申明的字符串地址传给ax寄存器
mov bp, ax ; ES:BP = 串地址
mov cx, 15 ; 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, memi OS!"
times 510 - ($-$$) db 0 ; 填充剩下的空间,使生成的二进制代码恰好为512字节(在nasm中,$$表示程序起始地址)
dw 0xaa55 ; 结束标志

编译运行

  在Linux下,安装nasm编译器、bximage磁盘工具、qemu模拟器:

1
> sudo apt-get install nasm bximage qemu

然后执行命令:

1
2
3
4
> nasm boot.asm -o boot.bin #编译生成机器码
> bximage -fd #创建软盘印象 a.img
> dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc #将boot程序写入软盘印象文件
> qemu -fda a.img #运行模拟器

就可以看到屏幕上输出的红色字符串”Hello, memi OS!”:图片挂掉了!!!

后话

  至此,我们的最简单的操作系统就完成了。当然,这个操作系统几乎什么都没干,所以严格来说它并不算一个操作系统。但当我们了解了操作系统的启动方法之后,其他功能的实现,就与普通的编程并没有太大区别了,无非就是把这里的显示字符串替换为其他功能而已。
  但是,其他功能的实现也依然是一个挑战。

参考文献

  1. 阮一峰的网络日志
  2. BIOS中断大全;
  3. 《一个操作系统的实现》第二版,电子工业出版社,于渊;
  4. 《Linux内核完全剖析基于0.12版》修正版3.0,赵炯。