读30天自制操作系统

1,624 阅读13分钟

第一天

当然,老实说这也算不上第一天,在一个星期以前我已经把这本书翻了一遍,在忙完了手中的事情后,也打算认真的实验一遍。

这本书虽然名字叫着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运行

VMware运行

QEMU运行

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

第四天

用C语言实现内存写入