0%

操作系统的引导

完成 bootsect.s 的屏幕输出功能

关键代码如下

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

! 文件:bootsect.s
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg1
mov ax,#0x07c0
mov es,ax
mov ax,#0x1301
int 0x10
inf_loop:
jmp inf_loop
msg1:
.byte 13,10
.ascii "LKJos is running..."
.byte 13,10,13,10
.org 510
boot_flag:
.word 0xAA55

我们修改的字符为19个,再加上前后一共3个回车加换行,所以总共25个字符。

将 .org 508 修改为 .org 510,是因为这里不需要 root_dev: .word ROOT_DEV,为了保证 boot_flag 一定在最后两个字节,所以要修改 .org。

执行下面两个命令编译和链接 bootsect.s

1
2
$ as86 -0 -a -o bootsect.o bootsect.s
$ ld86 -0 -s -o bootsect bootsect.o

需要留意的文件是 bootsect 的文件大小是 544 字节,而引导程序必须要正好占用一个磁盘扇区,即 512 个字节。 造成多了 32 个字节的原因是 ld86 产生的是 Minix 可执行文件格式, 这样的可执行文件处理文本段、数据段等部分以外,还包括一个 Minix 可执行文件头部。

去掉32字节的头部文件。

1
dd bs=1 if=bootsect of=Image skip=32

接下来进行拷贝,并且run

1
2
3
4
5
6
# 当前的工作路径为 /common/linux-0.11/boot/
# 将刚刚生成的 Image 复制到 linux-0.11 目录下
$ cp ./Image ../Image
# 执行run 脚本
$ ../../run

注:由于我执行run的时候有些许问题,所以我对于目录结构进行了修改。

效果如下图

image-20220508200758526

bootsect.s 读入 setup.s

首先编写一个 setup.s,该 setup.s 可以就直接拷贝前面的 bootsect.s,然后将其中的显示的信息改为:“Now we are in SETUP”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg2
mov ax,cs ! 这里的cs其实就是这段代码的段地址
mov es,ax
mov ax,#0x1301
int 0x10
inf_loop:
jmp inf_loop
msg2:
.byte 13,10
.ascii "Now we are in SETUP"
.byte 13,10,13,10
.org 510
boot_flag:
.word 0xAA55

接下来需要编写 bootsect.s 中载入 setup.s 的关键代码,注意去掉无限循环

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
SETUPLEN=2              ! 读入的扇区数
SETUPSEG=0x07e0 ! setup代码的段地址
entry _start
_start:
mov ah,#0x03 ! 设置功能号
xor bh,bh ! 将bh置0
int 0x10 ! 返回行号和列号,供显示串用
mov cx,#25 !要显示的字符串长度
mov bx,#0x0007 ! bh=0,bl=07(正常的黑底白字)
mov bp,#msg1 ! es:bp 要显示的字符串物理地址
mov ax,#0x07c0 ! 将es段寄存器置为#0x07c0
mov es,ax
mov ax,#0x1301 ! ah=13(设置功能号),al=01(目标字符串仅仅包含字符,属性在BL中包含,光标停在字符串结尾处)
int 0x10 ! 显示字符串

! 将setup模块从磁盘的第二个扇区开始读到0x7e00
load_setup:
mov dx,#0x0000 ! 磁头=0;驱动器号=0
mov cx,#0x0002 ! 磁道=0;扇区=2
mov bx,#0x0200 ! 偏移地址
mov ax,#0x0200+SETUPLEN ! 设置功能号;需要读出的扇区数量
int 0x13 ! 读磁盘扇区到内存
jnc ok_load_setup ! CF=0(读入成功)跳转到ok_load_setup
mov dx,#0x0000 ! 如果读入失败,使用功能号ah=0x00————磁盘系统复位
mov ax,#0x0000
int 0x13
jmp load_setup ! 尝试重新读入

ok_load_setup:
jmpi 0,SETUPSEG ! 段间跳转指令,跳转到setup模块处(0x07e0:0000)

! 字符串信息
msg1:
.byte 13,10 ! 换行+回车
.ascii "LKJos is running..."
.byte 13,10,13,10 ! 换行+回车

! 将
.org 510

! 启动盘具有有效引导扇区的标志。仅供BIOS中的程序加载引导扇区时识别使用。它必须位于引导扇区的最后两个字节中
boot_flag:
.word 0xAA55

效果如下图

image-20220508210219587

需要修改build.c,并且删除同目录下的 hdc-0.11.img.lock 即可。

setup.s 获取基本硬件参数

setup.s 将获得硬件参数放在内存的 0x90000 处。原版 setup.s 中已经完成了光标位置、内存大小、显存大小、显卡参数、第一和第二硬盘参数的保存。

ah=#0x03 调用 0x10 中断可以读出光标的位置,用 ah=#0x88 调用 0x15 中断可以读出内存的大小。 有些硬件参数的获取要稍微复杂一些,如磁盘参数表。在 PC 机中 BIOS 设定的中断向量表中 int 0x41 的中断向量位置( 4*0x41 = 0x0000:0x0104 )存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。 第二个硬盘的基本参数表入口地址存于 int 0x46 中断向量位置处。每个硬盘参数表有 16 个字节大小。

在PC机中BIOS设定的中断向量表中int 0x41的中断向量位置存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。对于100%兼容的BIOS来说,这里存放着硬盘参数表阵列的首地址0xF000:0E401,第二个硬盘的基本参数表入口地址存于int 0x46中断向量位置处.每个硬盘参数表有16个字节大小.

这段话是重点,磁盘参数就存放在以0x0000:0x0104为首地址的单元中只存了4个字节,里面存放的是磁盘参数表的偏移地址和段地址,也就是上文所说这里存放着硬盘参数表阵列的首地址0xF000:0E401。

以十六进制方式显示比较简单。这是因为十六进制与二进制有很好的对应关系(每 4 位二进制数和 1 位十六进制数存在一一对应关系),显示时只需将原二进制数每 4 位划成一组,按组求对应的 ASCII 码送显示器即可。ASCII 码与十六进制数字的对应关系为:0x30 ~ 0x39 对应数字 0 ~ 9,0x41 ~ 0x46 对应数字 a ~ f。从数字 9 到 a,其 ASCII 码间隔了 7h,这一点在转换时要特别注意。为使一个十六进制数能按高位到低位依次显示,实际编程中,需对 bx 中的数每次循环左移一组(4 位二进制),然后屏蔽掉当前高 12 位,对当前余下的 4 位(即 1 位十六进制数)求其 ASCII 码,要判断它是 0 ~ 9 还是 a ~ f,是前者则加 0x30 得对应的 ASCII 码,后者则要加 0x37 才行,最后送显示器输出。以上步骤重复 4 次,就可以完成 bx 中数以 4 位十六进制的形式显示出来。

如下图:

image-20220508215057266

代码如下

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
! 文件setup.s
INITSEG = 0x9000

entry _start
_start:
! 在显示字符串之前必须先获取当前光标的位置,这样就可以把字符串显示到当前光标处了
mov ah,#0x03
xor bh,bh
int 0x10

! 利用10号中断的13号功能打印字符串"Now we are in SETUP."
mov cx,#26
mov bx,#0x0007
mov bp,#msg1
mov ax,cs
mov es,ax
mov ax,#0x1301
int 0x10

! 下面开始读取一些硬件参数

! 读入光标位置信息,保存到0x90000处
mov ax,#INITSEG
mov ds,ax
mov ah,#0x03
xor bh,bh
int 0x10
mov [0],ds

! 读入内存大小位置信息,保存到0x90002处
mov ah,#0x88
int 0x15
mov [2],ax

! 从0x41处拷贝16个字节(磁盘参数表)到0x90004处
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0004
mov cx,#0x10
rep ! 重复16次
movsb


! 先打印光标位置
! 打印字符串之前要先读取光标位置,将字符串打印到当前光标处
mov ah,#0x03
xor bh,bh
int 0x10

! 打印字符串 "Cursor POS:"
mov cx,#11
mov bx,#0x0007
mov ax,cs
mov es,ax
mov bp,#msg2
mov ax,#0x1301
int 0x10

! 调用打印函数,打印光标位置
mov ax,#0x9000
mov ds,ax
mov dx,0x0
call print_hex
call print_nl

! 打印内存大小
! 打印字符串"Memory SIZE:"
mov ah,#0x03
xor bh,bh
int 0x10 ! 读取光标位置

mov cx,#12
mov bx,#0x0007
mov ax,cs
mov es,ax
mov bp,#msg3
mov ax,#0x1301
int 0x10


! 调用打印函数,打印内存大小信息
mov ax,#0x9000
mov ds,ax
mov dx,0x2
call print_hex


! 打印字符串"KB"
mov ah,#0x03
xor bh,bh
int 0x10 ! 读取光标位置

mov cx,#2
mov bx,#0x0007
mov ax,cs
mov es,ax
mov bp,#msg4
mov ax,#0x1301
int 0x10
call print_nl

!打印柱面数
! 打印字符串"Cyls"
mov ah,#0x03
xor bh,bh
int 0x10 ! 读取光标位置

mov cx,#5
mov bx,#0x0007
mov ax,cs
mov es,ax
mov bp,#msg5
mov ax,#0x1301
int 0x10


! 调用打印函数打印磁盘柱面数
mov ax,#0x9000
mov ds,ax
mov dx,0x4
call print_hex
call print_nl

! 打印磁头数
! 打印字符串"Heads:"
mov ah,#0x03
xor bh,bh
int 0x10 ! 读取光标位置

mov cx,#6
mov bx,#0x0007
mov ax,cs
mov es,ax
mov bp,#msg6
mov ax,#0x1301
int 0x10


! 调用打印函数打印磁盘磁头数
mov ax,#0x9000
mov ds,ax
mov dx,0x6
call print_hex
call print_nl


! 打印每磁道扇区数
! 打印字符串"sectors"
mov ah,#0x03
xor bh,bh
int 0x10 ! 读取光标位置

mov cx,#8
mov bx,#0x0007
mov ax,cs
mov es,ax
mov bp,#msg7
mov ax,#0x1301
int 0x10

! 调用打印函数打印扇区数
mov ax,#0x9000
mov ds,ax
mov dx,0x12
call print_hex
call print_nl

Inf_loop:
jmp Inf_loop ! 无限循环


! print_hex函数:将一个数字转换为ascii码字符,并打印到屏幕上
! 参数值:dx
! 返回值:无
print_hex:
mov cx,#4 ! 要打印4个十六进制数字,故循环4次
print_digit:
rol dx,#4 ! 循环以使低4比特用上 !! 取dx的高4比特移到低4比特处
mov ax,#0xe0f ! ah = 请求的功能值,al = 半字节(4个比特)掩码
and al,dl ! 取dl的低4比特值
add al,#0x30 ! 给al数字加上十六进制0x30
cmp al,#0x3a
jl outp ! 如果是一个不大于十的数字
add al,#0x07 ! 如果是a~f,要多加7
outp:
int 0x10
loop print_digit ! 用loop重复4次
ret

! 打印回车换行
print_nl:
mov ax,#0xe0d
int 0x10
mov al,#0xa
int 0x10
ret

msg1:
.byte 13,10
.ascii "Now we are in SETUP."
.byte 13,10,13,10

msg2:
.ascii "Cursor POS:"

msg3:
.ascii "Memory SIZE:"

msg4:
.ascii "KB"

msg5:
.ascii "Cyls:"

msg6:
.ascii "Heads:"

msg7:
.ascii "Sectors:"


.org 510
boot_flag:
.word 0xAA55

问题解答

1
当PC的电源打开后,80x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM—BIOS中的地址。PC机的BIOS将执行某些系统的检测,并在物理地址0处开始初始化中断向量。此后将启动设备的第一个扇区512字节读入内存绝对地址0x7C00处。因为当时system模块的长度不会超过0x80000字节大小512KB,所以bootsect程序把system模块读入物理地址0x10000开始位置处时并不会覆盖在0x90000处开始的bootsect和setup模块,多此一举的是system模块移到内存中相对靠后的位置,以便加载系统主模块。解决方案是在保证操作系统启动引导成功的前提下尽量扩大ROM—BIOS的内存寻址范围。

知识点记录

数据寄存器

AH&AL=AX(accumulator):累加寄存器,常用于运算;在乘除等指令中指定用来存放操作数,另外,所有的I/O指令都使用这一寄存器与外界设备传送数据。

BH&BL=BX(base):基址寄存器,常用于地址索引

CH&CL=CX(count):计数寄存器,常用于计数;常用于保存计算值,如在移位指令,循环(loop)和串处理指令中用作隐含的计数器.

DH&DL=DX(data):数据寄存器,常用于数据传递。

他们的特点是,这4个16位的寄存器可以分为高8位: AH, BH, CH, DH.以及低八位:AL,BL,CL,DL。这2组8位寄存器可以分别寻址,并单独使用。

中断知识

int 0x10

image-20220509162715086

注意,这里ah要先有值,代表内部子程序的编号

功能号ah=0x03,作用是读取光标的位置

输入:

1
bh = 页号

返回:

1
ch = 扫描开始线;cl = 扫描结束线;dh = 行号;dl = 列号

功能号ah=0x13,作用是显示字符串

输入:

1
al = 放置光标的方式及规定属性,下文 al=1,表示目标字符串仅仅包含字符,属性在BL中包含,光标停在字符串结尾处;es:bp = 字符串起始位置;cx = 显示的字符串字符数;bh = 页号;bl = 字符属性,下文 bl = 07H,表示正常的黑底白字;dh = 行号;dl = 列号

功能号ah=0x0e,作用是显示字符

输入:

1
al = 字符

int 0x13

在DOS等实模式操作系统下,调用INT 13h会跳转到计算机的ROM-BIOS代码中进行低级磁盘服务,对程序进行基于物理扇区的磁盘读写操作。

功能号ah=0x02,作用是读磁盘扇区到内存
输入:

寄存器 含义
ah 读磁盘扇区到内存
al 需要读出的扇区数量
ch 磁道
cl 扇区
dh 磁头
dl 驱动器
es:bx 数据缓冲区的地址

返回

1
ah = 出错码(00H表示无错,01H表示非法命令,02H表示地址目标未发现…);CF为进位标志位,如果没有出错CF=0

功能号ah=0x00,作用是磁盘系统复位

1
2
输入:dl = 驱动器
返回:如果操作成功———— C F = 0 CF=0CF=0,a h = 00 H ah=00Hah=00H

int 0x15

功能号ah=0x88,作用是获取系统所含扩展内存大小

1
2
输入:ah = 0x88
返回:ax = 从0x100000(1M)处开始的拓展内存大小(KB)。若出错则CF置位,ax = 出错码。

int 0x41

在PC机中BIOS设定的中断向量表中int 0x41的中断向量位置 (4 ∗ 0 x 41 = 0 x 0000 : 0 x 0104 4*0x41 = 0x0000:0x01044∗0x41=0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。对于100%兼容的BIOS来说,这里存放着硬盘参数表阵列的首地址0xF000:0E401,第二个硬盘的基本参数表入口地址存于int 0x46中断向量位置处.每个硬盘参数表有16个字节大小.

其他

lds

格式: LDS reg16,mem32

其意义是同时给一个段寄存器和一个16位通用寄存器同时赋值

举例:

地址 100H 101H 102H 103H
内容 00H 41H 02H 03H
1
2
3
4
5
LDS AX,[100H]
! 结果:AX=4100H DS=0302H
可以理解为
mov AX,[100H]
mov DS,[100H+2]