
第一天
当然,老实说这也算不上第一天,在一个星期以前我已经把这本书翻了一遍,在忙完了手中的事情后,也打算认真的实验一遍。
这本书虽然名字叫着30天自制操作系统,但事实上在这30天之前你需要有一些知识的沉淀,如果没有的话,或许要得多一段时间吧。
我也把书中的光盘文件上传到了github,链接:github.com/imangoa/os。嗯,如果要看书的话,我建议到微信读书里,用不完的体验卡,哈哈。
编写hello world
书中要完成的第一个任务就是在打开电源时显示 hello world,就像我们以前学习一门新语言一样。
printf("hello world");
system.out.println("hello world");
print("...");
而在这本书地第一章,提供了三种方式去完成这个。当然,这三种方式也是殊途同归。
- 第一种是通过编辑二进制的方式,按照作者给的二进制值输入,最后把文件后缀改为.img。当然,作者也没打算我们真的这么去做,以这种诙谐的手法,可能是旨在告诉读者一切程序都由二进制构成。
第一种太长了,就不写下来了,放个截图。感兴趣的伙伴可以用二进制编辑器打开镜像文件直接查看。
- 第二种使用的是汇编,不过是使用汇编的RESB标识符和DB标识符进行写二进制值,RESB符填充了为00的二进制位。本质上与第一种方式并没有区别。
DB 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f DB 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01, 0x01, 0x00 DB 0x02, 0xe0, 0x00, 0x40, 0x0b, 0xf0, 0x09, 0x00 DB 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 DB 0x40, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x29, 0xff DB 0xff, 0xff, 0xff, 0x48, 0x45, 0x4c, 0x4c, 0x4f DB 0x2d, 0x4f, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41 DB 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 RESB 16 DB 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09 DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb DB 0xee, 0xf4, 0xeb, 0xfd, 0x0a, 0x0a, 0x68, 0x65 DB 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72 DB 0x6c, 0x64, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 368 DB 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 4600 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 1469432
- 第三种方式也是通过汇编,与第一种和第二种并无本质区别。其实也就是第二种方式上把代码整理的更符合人类编码方式,知其然,知其所以然。第三种就是知其所以然吧。老规矩,上代码。
; hello-os ; TAB=4 ; 以下这段是标准FAT12格式软盘专用代码 DB 0xeb, 0x4e, 0x90 DB "HELLOIPL" ; 启动区的名称可以是任意的字符串(8字节) DW 512 ; 每个扇区的大小(必须是512字节) DB 1 ; 簇的大小(必须为1扇区) DW 1 ; FAT的起始位置(一般从第一个扇区开始) DB 2 ; FAT的个数(必须为而) DW 224 ; 根目录的大小(一般设成224项) DW 2880 ; 该磁盘的大小(必须是2880扇区) DB 0xf0 ; 磁盘的种类(必须是0xf0) DW 9 ; FAT的长度(必须是9扇区) DW 18 ; 1个磁道有几个扇区(必须是18) DW 2 ; 磁头数(必须是2) DD 0 ; 不使用分区,必须是0 DD 2880 ; 重写一次磁盘的大小 DB 0,0,0x29 ; 意义不明,固定 DD 0xffffffff ; (可能是)卷标号码 DB "HELLO-OS " ; 磁盘的名称(11字节) DB "FAT12 " ; 磁盘格式名称(8字节) RESB 18 ; 先空出18字节 ; 程序本体 DB 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09 DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb DB 0xee, 0xf4, 0xeb, 0xfd ; 信息显示部分 DB 0x0a, 0x0a ; 两个换行 DB "hello, world" DB 0x0a ; 换行 DB 0 RESB 0x1fe-$ ; 填写0xoo,直到0x001fe DB 0x55, 0xaa ; 以下是启动区以外部分的输出 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 4600 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 1469432
当然,虽然是三种方法,但其实本质都和第一种是一样的。都只是在排列二进制位。
运行hello world
很巧的是运行也有三种方法,第一种是在QEMU仿真器上运行,第二种是可以通过在vmware上面的软盘挂载运行,第三种是可以通过把img写入到U盘上,插入到真机上u盘启动运行。QEMU上运行会比较简单,而在真机上运行成就感或许会大。所以,我选在VMware上运行,哈哈!
放一波截图吧,hh
VMware运行

QEMU运行

一波操作下来,第一天也就完成了!至于怎么在VMware上运行和真机上运行,可以百度搜索一下。我就不做教程的搬运工了
第二天
前面讲过两种使用汇编显示hello world的方式,但是可能下面这种才是真正符合汇编逻辑的代码。前面的那两种,本质上也只是用汇编的方式去编辑二进制值。
; TAB=4
ORG 0x7c00 ; 指明程序的装载地址
; 以下的记述用于标准FAT12格式的软盘
JMP entry
DB 0x90
——-(中略)——-
; 程序核心
entry:
MOV AX,0 ; 初始化寄存器
MOV SS, AX
MOV SP,0x7c00
MOV DS, AX
MOV ES, AX
MOV SI, msg
putloop:
MOV AL, [SI]
ADD SI,1 ; 给SI加1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
fin:
HLT ; 让CPU停止,等待指令
JMP fin ; 无限循环
msg:
DB 0x0a, 0x0a ; 换行2次
DB "hello, world"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 0x7dfe-0x7c00=510
DB 0x55, 0xaa ;2 byte
org伪指令
在我第一次看这段代码的时候,我以为org指令是表示把这个程序装载到0X7c00这个地址上,但恰巧我也同时在看Linux内核设计的艺术,对于0X7c00这个地址还是有点印象的。
cpu在接收到int 0x19中断后,调用中断服务程序,将软盘第一扇区(512B)的程序加载到0X07c00这个地址上。
ORG是Origin的缩写:起始地址源。在汇编语言源程序的开始通常都用一条ORG伪指令来实现规定程序的起始地址。如果不用ORG规定则汇编得到的目标程序将从0000H开始。
因为这段程序将会加载到0x07c00上,所以使用org 0x7c00来规范代码。而不是使用org 0x7c00,程序才会加载到0x07c00上。当使用org后,entry=entry+0x7c00。
执行代码
把 OS\projects\02_day\helloos4 里面的文件放到 OS\tolset\helloos1 里面,双击cons_nt.bat,依次输入asm,makeimg,run。撒花!当然,感兴趣的也可以查看一下bat文件的内容,逻辑都挺简单的。
使用makefile
作者觉得使用批处理比较繁琐,就向我们介绍了makefile。嗯,就是Linux C的那个makefile。
第一个makefile文件
#文件生成规则
ipl.bin : ipl.nas Makefile
../z_tools/nask.exe ipl.nas ipl.bin ipl.lst
helloos.img : ipl.bin Makefile
../z_tools/edimg.exe imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl.bin len:512 from:0 to:0 imgout:helloos.img
如果,你打开helloos4里面的asm.bat和makeimg.bat,会发现它们和makefile文件里面的ipl.bin和helloos.img内容是一样的。
那么如何去执行它们呢?
make -r ipl.bin
make -r helloos.img
为了避免每次都输入 -r 执行,作者为makefile添加了以下的内容。
asm :
../z_tools/make.exe -r ipl.bin
img :
../z_tools/make.exe -r helloos.img
run :
../z_tools/make.exe img
copy helloos.img ..\z_tools\qemu\fdimage0.bin
../z_tools/make.exe -C ../z_tools/qemu
逻辑很好理解,现在我们只需要输入 make run 就可以直接在qemu上运行我们编写的代码啦。不过,makefile文件后面还有几行代码是做什么的呢?
install :
../z_tools/make.exe img
../z_tools/imgtol.com w a: helloos.img
clean :
-del ipl.bin
-del ipl.lst
src_only :
../z_tools/make.exe clean
-del helloos.img
install 将磁盘映像文件写入磁盘。我们以后在操作系统上开发的程序写入到镜像文件。
clean 命令的作用是删除文件夹下面编译出现的ipl.bin ,ipl.list 文件
src_only命令是在clean的基础上删除helloos.img文件
那些中途生成的文件
ipl.bin是汇编代码编译后的机器代码
ipl.lst是每句汇编指令和机器指令的对照

作者说
我把我感觉作者说的很有道理的话摘抄下来啦。
这么一来,说不定我们拿数码相机拍一幅风景照,把它作为磁盘映像文件保存到磁盘里,就能成为世界上最优秀的操作系统!这看似荒谬的情况也是有可能发生的。但从常识来看,这样做成的东西肯定会故障百出。反之,我们把做出的可执行文件作为一幅画来看,也没准能成为世界上最高水准的艺术品.不过可以想象的是,要么文件格式有错,要么显示出来的图是乱七八糟的。
第三天
制作真正的IPL(Initial Program Loader)
前面两天不是在写hello world,就是在写helloworld的路上。但是我们要做的是一个操作系统,而不是在屏幕上显示字符串。那么,如何做呢?我们在bios的帮助下,已经把磁盘的第一扇区加载到了内存,那我们要做的肯定是把磁盘上更多的内容加载到内存上呐。
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
这段代码的意思是,将第二扇区的内容读入到0x08200地址上。
int 13中断
磁盘读、写,扇区校验(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)功能一样)
试错
软盘这东西很不可靠,有时会发生不能读数据的状况,这时候重新再读一次就行了。
代码
;读磁盘
MOV AX,0x0820
MOV ES, AX
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
MOV SI,0 ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; AH=0x02 : 读入磁盘
MOV AL,1 ; 1个扇区
MOV BX,0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOS
JNC fin ; 没出错的话跳转到fin
ADD SI,1 ; 往SI加1
CMP SI,5 ; 比较SI与5
JAE error ; SI >= 5时,跳转到error
MOV AH,0x00
MOV DL,0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry
其实,与上面的代码的区别只是增加了一个判断位,若读取次数低于5次,读取失败则重置驱动器重新读取。若大于5次,则跳转到error(error负责显示错误)。
读取更多的内容
作者读取了10个柱面上的18个扇区的数据到内存,代码逻辑相比读取一个扇区并没有多大的改变。
;读磁盘
MOV AX,0x0820
MOV ES, AX
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
readloop:
MOV SI,0 ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; AH=0x02 : 读入磁盘
MOV AL,1 ; 1个扇区
MOV BX,0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOS
JNC next ; 没出错时跳转到next
ADD SI,1 ; SI加1
CMP SI,5 ; 比较SI与5
JAE error ; SI >= 5时,跳转到error
MOV AH,0x00
MOV DL,0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry
next:
MOV AX, ES ; 把内存地址后移0x200
ADD AX,0x0020
MOV ES, AX ; 因为没有ADD ES,0x020指令,所以这里稍微绕个弯
ADD CL,1 ; CL加1
CMP CL,18 ; 比较CL与18
JBE readloop ; 如果CL<= 18,则跳转至readloop
MOV CL,1
ADD DH,1
CMP DH,2
JB readloop ; 如果DH < 2,则跳转到readloop
MOV DH,0
ADD CH,1
CMP CH, CYLS ;CYLS=10 因为前面声明过 CYLS EQU 10
JB readloop ; 如果CH < CYLS,则跳转到readloop
如果算上系统加载时自动装载的启动扇区,那现在我们已经能够把软盘最初的10× 2× 18× 512=184320 byte=180KB内容完整无误地装载到内存里了。
我想比较多的跳转指令,可能容易弄糊涂,所以把上面用到的指令都列下来了
JC ;当运算产生进位标志时,即CF=1时,跳转到目标程序处。
JNC ;当CF=0时,跳转到目标程序处。
JBE ;如小于等于则跳转
JB; 如小于则跳转
JAE; 如大于等于则跳转
感觉自己成了一无四处的代码搬用工了,啊啊啊,要改变风格啦。
最简单的操作系统
载入一部分软盘到内存后,新建haribote.nas文件
fin:
HLT
JMP fin
HLT
停止执行指令,执行后cpu进入停止状态。不再执行指令。 直到被其他设备的信号或中断信号来激活。用来等待设备输入和节能
编译haribote.nas,查看haribote.lst。

在执行make img后,我们发现
0x002600附近,磁盘的这个位置保存着文件名:haribote.sys。

0x004200那里,可以看到“F4 EB FD”。

由于该文件内容在磁盘映像中地址是0x004200,所以在内存中应该是0x8000+0x4200=0xc200。所以,我们要给上面的代码加上org 0xc200,再给ipl.nas加上jmp 0xc200。然后make run。

没啥回显,我也不知道成不成功。那就让它显示全黑看看。
显示全黑
怎么改变屏幕的显示呢,Bios中断吧。
; haribote-os
; TAB=4
ORG 0xc200 ; 这个程序将要被装载到内存的什么地方呢?
MOV AL,0x13 ; VGA显卡,320x200x8位彩色
MOV AH,0x00 ; 功能00h,设置显示器模式
INT 0x10
fin:
HLT
JMP fin

为32位做准备
32位模式下可以使用的内存容量远远大于1MB。另外,CPU的自我保护功能(识别出可疑的机器语言并进行屏蔽,以免破坏系统)在16位下不能用,但32位下能用。既然有这么多优点,当然要使用32位模式了。
所以,我们需要在使用32位之前,把我们在16位的设置记录下来。
; haribote-os
; TAB=4
; 有关BOOT_INFO
CYLS EQU 0x0ff0 ; 设定启动区
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色数目的信息。颜色的位数。
SCRNX EQU 0x0ff4 ; 分辨率的X(screen x)
SCRNY EQU 0x0ff6 ; 分辨率的Y(screen y)
VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址
ORG 0xc200 ; 这个程序将要被装载到内存的什么地方呢?
MOV AL,0x13 ; VGA显卡,320x200x8位彩色
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; 记录画面模式
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000
;用BIOS取得键盘上各种LED指示灯的状态
MOV AH,0x02
INT 0x16 ; keyboard BIOS
MOV [LEDS], AL
fin:
HLT
JMP fin