前言

​ 手写操作系统,听起来就是一件不可能完成的工作。但如果不去了解操作系统也就无法写出高效率的程序。手写操作系统注定是一个非常艰辛的过程或者说回报是不确定的。就个人而言我比较喜欢去了解事物发生的源头,所以不去了解底层原理我的大脑中就会一直有疑问。

​ 虽说现在学习资源很好获得,但你能用的又有多少,其实到最后你可能发现谁都无法帮你解决问题,到最后都是你自己解决问题。网上的学习资料鱼龙混杂你根本找不到你真正所需要的,搞不好会落入营销号的圈套。到最后都会回归到书本,你最讨厌学的,最枯燥的往往是重要的。

​ 现在我基于《30天自制操作系统》这本书去实践写一个简单的操作系统。

​ 这是一个挑战,我无法确定我是否可以完成这个艰巨的任务。

准备工作

在这本书中的内容无法完全采用。

所使用的工具gcc,make,nasm,bochs

平台:ubuntu

nask和nasm的区别

这里不使用书中的nask编译器,直接使用nasm编译器。nask和nasm区别不大。其中主要的差别如下:

1
2
3
4
5
6
7
8
9
10
nask代码              NASM代码


JMP entry -> JMP SHORT entry

RESB <填充字节数> -> TIMES <填充字节数> DB <填充数据>

RESB 0x7dfe-$ -> TIMES 0x1fe-($-$$) DB 0 ;

ALIGNB 16 -> ALIGN 16, DB 0

如何启动bochs

启动Bochs虚拟机

  • 显式方式:
    bochs -f bochsrc_file

  • 隐式方式:
    bochs

  • 当前目录下的启动文件优先级:

    (从上往下依次查找当前目录下是否有如下启动文件)

    • .bochsrc
    • bochsrc
    • bochsrc.txt

其中bochs启动配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注意'#'后面为注内容

megs:128
#模拟器的内存

romimage:file=/usr/share/bochs/BIOS-bochs-latest
#这个是BIOS-bochs-latest的路径,自己慢慢找,不一定和我的一样

vgaromimage:file=/usr/share/bochs/VGABIOS-lgpl-latest
#这个是VGABIOS-lgpl-latest的路径,自己慢慢找

floppya:1_44=bootroot-0.11,status=inserted
#这个是启动软盘,就是我们下载的那个,就在当前目录下,如果不在当前目录,需要指明路径

boot:floppy
#表示从软盘启动

log:bochsout.txt
#日志输出文件

#选项还有很多,想了解更多可以参照原始的.bochsrc(在bochs-2.4.5/目录下)

目前可能我们还缺软盘,好在bochs中bximage命令可以制作软盘。Bochs 自带了一个叫做 bximage 的工具,直接在控制台执行,就会看到一个 TUI 向导:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
========================================================================
bximage
Disk Image Creation / Conversion / Resize and Commit Tool for Bochs
$Id: bximage.cc 13481 2018-03-30 21:04:04Z vruppert $
========================================================================

1. Create new floppy or hard disk image
2. Convert hard disk image to other format (mode)
3. Resize hard disk image
4. Commit 'undoable' redolog to base image
5. Disk image info

0. Quit

Please choose one [0]

Bochs中的常用调试命令

命令 功能 示例
b(break) 设置断点 b 0x7c00
c(continue) 继续执行 c
s(step) 单步执行 s
info b(info break) 查看当前所有断点 info b
info cpu 查看当前CPU状态 info cpu
r(reg) 查看常规寄存器状态 r
sreg 查看段寄存器状态 sreg
x /Nuf expression 查看内存中的数据 x /2bx 0x7c00
trace on[off] 开关:打开执行的指令 trace on
trace-reg on[off] 开关:打开寄存器的值 trace-reg on

第一天

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
; hello-os
; TAB=4

; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code

DB 0xeb, 0x4e, 0x90
DB "HELLOIPL" ; 启动扇区名称(8字节)
DW 512 ; 每个扇区(sector)大小(必须512字节)
DB 1 ; 簇(cluster)大小(必须为1个扇区)
DW 1 ; FAT起始位置(一般为第一个扇区)
DB 2 ; FAT个数(必须为2)
DW 224 ; 根目录大小(一般为224项)
DW 2880 ; 该磁盘大小(必须为2880扇区1440*1024/512)
DB 0xf0 ; 磁盘类型(必须为0xf0)
DW 9 ; FAT的长度(必须是9扇区)
DW 18 ; 一个磁道(track)有几个扇区(必须为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-$ ; 填写0x00直到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

第二天

简单介绍汇编和makefile的使用

代码ipl.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
; hello-os
; TAB=4

ORG 0x7c00 ; 指明程序装载地址

; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code

JMP entry
DB 0x90
DB "HELLOIPL" ; 启动扇区名称(8字节)
DW 512 ; 每个扇区(sector)大小(必须512字节)
DB 1 ; 簇(cluster)大小(必须为1个扇区)
DW 1 ; FAT起始位置(一般为第一个扇区)
DB 2 ; FAT个数(必须为2)
DW 224 ; 根目录大小(一般为224项)
DW 2880 ; 该磁盘大小(必须为2880扇区1440*1024/512)
DB 0xf0 ; 磁盘类型(必须为0xf0)
DW 9 ; FAT的长度(必??9扇区)
DW 18 ; 一个磁道(track)有几个扇区(必须为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字节

; 程序主体

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 ; 换行两次
DB "hello, world"
DB 0x0a ; 换行
DB 0

RESB 0x7dfe-$ ; 填写0x00直到0x001fe

DB 0x55, 0xaa

第三天

asmhead.nas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
; haribote-os boot asm
; TAB=4

BOTPAK EQU 0x00280000 ; 加载bootpack
DSKCAC EQU 0x00100000 ; 磁盘缓存的位置
DSKCAC0 EQU 0x00008000 ; 磁盘缓存的位置(实模式)

; BOOT_INFO相关
CYLS EQU 0x0ff0 ; 引导扇区设置
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色的信息
SCRNX EQU 0x0ff4 ; 分辨率X
SCRNY EQU 0x0ff6 ; 分辨率Y
VRAM EQU 0x0ff8 ; 图像缓冲区的起始地址

ORG 0xc200 ; 这个的程序要被装载的内存地址

; 画面モードを設定

MOV AL,0x13 ; VGA显卡,320x200x8bit
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; 屏幕的模式(参考C语言的引用)
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000

; 通过BIOS获取指示灯状态

MOV AH,0x02
INT 0x16 ; keyboard BIOS
MOV [LEDS],AL

; 防止PIC接受所有中断
; AT兼容机的规范、PIC初始化
; 然后之前在CLI不做任何事就挂起
; PIC在同意后初始化

MOV AL,0xff
OUT 0x21,AL
NOP ; 不断执行OUT指令
OUT 0xa1,AL

CLI ; 进一步中断CPU

; 让CPU支持1M以上内存、设置A20GATE

CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout

; 保护模式转换

[INSTRSET "i486p"] ; 说明使用486指令

LGDT [GDTR0] ; 设置临时GDT
MOV EAX,CR0
AND EAX,0x7fffffff ; 使用bit31(禁用分页)
OR EAX,0x00000001 ; bit0到1转换(保护模式过渡)
MOV CR0,EAX
JMP pipelineflush
pipelineflush:
MOV AX,1*8 ; 写32bit的段
MOV DS,AX
MOV ES,AX
MOV FS,AX
MOV GS,AX
MOV SS,AX

; bootpack传递

MOV ESI,bootpack ; 源
MOV EDI,BOTPAK ; 目标
MOV ECX,512*1024/4
CALL memcpy

; 传输磁盘数据

; 从引导区开始

MOV ESI,0x7c00 ; 源
MOV EDI,DSKCAC ; 目标
MOV ECX,512/4
CALL memcpy

; 剩余的全部

MOV ESI,DSKCAC0+512 ; 源
MOV EDI,DSKCAC+512 ; 目标
MOV ECX,0
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; 除以4得到字节数
SUB ECX,512/4 ; IPL偏移量
CALL memcpy

; 由于还需要asmhead才能完成
; 完成其余的bootpack任务

; bootpack启动

MOV EBX,BOTPAK
MOV ECX,[EBX+16]
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
JZ skip ; 传输完成
MOV ESI,[EBX+20] ; 源
ADD ESI,EBX
MOV EDI,[EBX+12] ; 目标
CALL memcpy
skip:
MOV ESP,[EBX+12] ; 堆栈的初始化
JMP DWORD 2*8:0x0000001b

waitkbdout:
IN AL,0x64
AND AL,0x02
JNZ waitkbdout ; AND结果不为0跳转到waitkbdout
RET

memcpy:
MOV EAX,[ESI]
ADD ESI,4
MOV [EDI],EAX
ADD EDI,4
SUB ECX,1
JNZ memcpy ; 运算结果不为0跳转到memcpy
RET
; memcpy地址前缀大小

ALIGNB 16
GDT0:
RESB 8 ; 初始值
DW 0xffff,0x0000,0x9200,0x00cf ; 写32bit位段寄存器
DW 0xffff,0x0000,0x9a28,0x0047 ; 可执行的文件的32bit寄存器(bootpack用)

DW 0
GDTR0:
DW 8*3-1
DD GDT0

ALIGNB 16
bootpack:

bootpack.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 告诉C编译器,有一个函数在别的文件里 */

void io_hlt(void);

/* 是函数声明却不用{},而用;,这表示的意思是:
函数在别的文件中,你自己找一下 */

void HariMain(void)
{

fin:
io_hlt(); /* 执行naskfunc.nas中的_io_hlt函数 */
goto fin;

}

ipl10.nas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
; haribote-ipl
; TAB=4

CYLS EQU 10 ; 声明CYLS=10

ORG 0x7c00 ; 指明程序装载地址

; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code

JMP entry
DB 0x90
DB "HARIBOTE" ; 启动扇区名称(8字节)
DW 512 ; 每个扇区(sector)大小(必须512字节)
DB 1 ; 簇(cluster)大小(必须为1个扇区)
DW 1 ; FAT起始位置(一般为第一个扇区)
DB 2 ; FAT个数(必须为2)
DW 224 ; 根目录大小(一般为224项)
DW 2880 ; 该磁盘大小(必须为2880扇区1440*1024/512)
DB 0xf0 ; 磁盘类型(必须为0xf0)
DW 9 ; FAT的长度(必??9扇区)
DW 18 ; 一个磁道(track)有几个扇区(必须为18)
DW 2 ; 磁头数(必??2)
DD 0 ; 不使用分区,必须是0
DD 2880 ; 重写一次磁盘大小
DB 0,0,0x29 ; 意义不明(固定)
DD 0xffffffff ; (可能是)卷标号码
DB "HARIBOTEOS " ; 磁盘的名称(必须为11字?,不足填空格)
DB "FAT12 " ; 磁盘格式名称(必??8字?,不足填空格)
RESB 18 ; 先空出18字节

; 程序主体

entry:
MOV AX,0 ; 初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX

; 读取磁盘

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(512/16十六进制转换)
ADD AX,0x0020
MOV ES,AX ; ADD ES,0x020因为没有ADD ES,只能通过AX进行
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
JB readloop ; CH < CYLS 跳转到readloop

; 读取完毕,跳转到haribote.sys执行!
MOV [0x0ff0],CH ; IPLがどこまで読んだのかをメモ
JMP 0xc200

error:
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 ; 换行两次
DB "load error"
DB 0x0a ; 换行
DB 0

RESB 0x7dfe-$ ; 填写0x00直到0x001fe

DB 0x55, 0xaa

naskfunc.nas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
; naskfunc
; TAB=4

[FORMAT "WCOFF"] ; 制作目标文件的模式
[BITS 32] ; 制作32位模式用的机器语言


; 制作目标文件的信息

[FILE "naskfunc.nas"] ; 源文件名信息

GLOBAL _io_hlt ; 程序中包含的函数名


; 以下是实际的函数

[SECTION .text] ; 目标文件中写了这些后再写程序

_io_hlt: ; void io_hlt(void);
HLT
RET

第四天

第五天

第六天

第七天