操作系统的引导
# 1. 操作系统的引导
此次实验的基本内容是:
- 阅读《Linux 内核完全注释》的第 6 章,对计算机和 Linux 0.11 的引导过程进行初步的了解;
- 按照下面的要求改写 0.11 的引导程序 bootsect.s
- 有兴趣同学可以做做进入保护模式前的设置程序 setup.s。
在实验报告中回答如下问题:
- 有时,继承传统意味着别手蹩脚。x86 计算机为了向下兼容,导致启动过程比较复杂。请找出 x86 计算机启动过程中,被硬件强制,软件必须遵守的两个“多此一举”的步骤(多找几个也无妨),说说它们为什么多此一举,并设计更简洁的替代方案。 评分标准
- bootsect 显示正确,30%
- bootsect 正确读入 setup,10%
- setup 获取硬件参数正确,20%
- setup 正确显示硬件参数,20%
- 实验报告,20%
# 改写 bootsect.s
bootsect.s 能在屏幕上打印一段提示信息“XXX is booting...”,其中 XXX 是你给自己的操作系统起的名字,例如 LZJos、Sunix 等(可以上论坛上秀秀谁的 OS 名字最帅,也可以显示一个特色 logo,以表示自己操作系统的与众不同。)
# 简单版 v1.0
简单的话,我们直接改版原本的 bootsect.s 的字符串和字符串个数即可,步骤如下:
cd ~/oslab/linux-0.11/boot
vim bootsect.s
2
修改 msg1 的内容,同时修改 cx 的值
修改246的字符串为 "POS is runing"
第98行 mov cx,#23
2
然后保存并退出 vim
重新编译
as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
dd bs=1 if=bootsect of=Image skip=32
2
3
我们可以写个 shell 脚本,这样以后每次编译的时候,就不用写上述命令了,我这里新建一个 lab1shell.sh,里面放上面 3 条命令
生成内核
cd ~/oslab/linux-0.11
make all
2
可以看到 Image 的时间已经是被更新为你编译的时候的时间
$ ll
-rw-rw-r-- 1 peterjxl peterjxl 128161 10月 28 07:54 Image
2
运行 Bochs
cd ~/oslab
./run
2
实验截图
# 不太简单版 v2.0
接下来我们重写 bootsect.s ,只完成关键的功能即可
首先完成屏幕输出功能:屏幕输出的功能主要是用 10 号中断
! 首先读入光标位置
mov ah, #0x03
xor bh, bh
int 0x10
2
3
4
输入参数:bh 是页号,这里置 0
返回参数:CH=光标起始位置,CL=光标结束位置,DH=光标行号(0-based),DL=光标列号(0-based)
然后定义字符串:
msg1: .byte 13, 10
.ascii "Hello OS World, my name is JXL"
.byte 13, 10, 13, 10
.org 510
boot_flag: .word 0xAA55
2
3
4
5
6
7
.byte 13,10 就是定义个回车和换行,第 4 行就是两个回车和换行
.org 510 伪指令,表示在它之后的指令从地址 510 开始存放。遇到.org,编译器会把其后的指令代码放到 org 伪指令指定的偏移地址。如 org 指定的地址和之前的指令地址有空洞,则用 0 填充。
boot_flag: .word 0xAA55:是启动盘具有有效引导扇区的标志。仅供 BI0S 中的程序加载引导扇区时识别使用。它必须位于引导扇区的最后两个字节中。
然后是显示字符串
! 显示字符串
mov cx, #36
mov bx, #0x0007
mov ax, #0x7c00
mov es, ax
mov bp, #msg1
mov ax, #0x1301
int 0x10
2
3
4
5
6
7
8
调用参数:cx 是字符串长度,BH=页号,BL=显示属性,,es:bp 是字符串的地址,ax 是功能号 显示字符串光标跟随移动
显示完字符串后,我们不再往下执行(例如载入 setup.s 和把自己挪到 0x9000 处),我们这里可以写个简单的死循环
inf_loop:
jmp inf_loop
2
完整代码如下:
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#36
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 "Hello OS world, my name is LZJ"
.byte 13,10,13,10
.org 510
boot_flag: .word 0xAA55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
我们重新编译和运行,实验结果如下
运行后,就不会继续往下有输出了,因为是死循环。
# 2. 改写 setup.s
改写 setup.s 主要完成如下功能:
- bootsect.s 能完成 setup.s 的载入,并跳转到 setup.s 开始地址执行。而 setup.s 向屏幕输出一行"Now we are in SETUP"。
- setup.s 能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
- setup.s 不再加载 Linux 内核,保持上述信息显示在屏幕上即可。
# 加载 setup.s 版 v1.0
同样是显示字符串,因此可以直接复用 bootsect.s 的 2.0 版本,改改显示的字符串即可。我们可以直接删掉 setup.s,然后 cp 一份 bootsect.s
$ cat bootsect.s
$ cp bootsect.s setup.s
2
然后有 4 个地方要修改:
完整代码:
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg2
mov ax,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
修改 bootsect.s 加载 setup 模块 v3.0,我们可以参考原版的 bootsect.s 的模块,我们只需要将其 copy 到我们自己的 bootsect.s 即可……
load_setup:
! 设置驱动器和磁头(drive 0, head 0): 软盘 0 磁头
mov dx,#0x0000
! 设置扇区号和磁道(sector 2, track 0): 0 磁头、0 磁道、2 扇区
mov cx,#0x0002
! 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节
mov bx,#0x0200
! 设置读入的扇区个数(service 2, nr of sectors),
! SETUPLEN是读入的扇区个数,Linux 0.11 设置的是 4,
! 我们不需要那么多,我们设置为 2(因此还需要添加变量 SETUPLEN=2)
mov ax,#0x0200+SETUPLEN
! 应用 0x13 号 BIOS 中断读入 2 个 setup.s扇区
int 0x13
! 读入成功,跳转到 ok_load_setup: ok - continue
jnc ok_load_setup
! 软驱、软盘有问题才会执行到这里。我们的镜像文件比它们可靠多了
mov dx,#0x0000
! 否则复位软驱 reset the diskette
mov ax,#0x0000
int 0x13
! 重新循环,再次尝试读取
jmp load_setup
ok_load_setup:
! 接下来要干什么?当然是跳到 setup 执行。
! 要注意:我们没有将 bootsect 移到 0x9000,因此跳转后的段地址应该是 0x7ce0
! 即我们要设置 SETUPSEG=0x07e0
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
完整的 bootsect.s 代码如下:
SETUPLEN=2
SETUPSEG=0x07e0
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#36
mov bx,#0x0007
mov bp,#msg1
mov ax,#0x07c0
mov es,ax
mov ax,#0x1301
int 0x10
load_setup:
mov dx, #0x0000 ; 设置驱动器和磁头(drive 0, head 0): 软盘 0 磁头
mov cx, #0x0002 ; 设置扇区号和磁道(sector 2, track 0): 0 磁头、0 磁道、2 扇区
mov bx, #0x0200 ; 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节
mov ax, #0x0200 + SETUPLEN; 设置读入的扇区个数(service 2, nr of sectors),SETUPLEN是读入的扇区个数,Linux 0.11 设置的是 4,我们不需要那么多,我们设置为 2(因此还需要添加变量 SETUPLEN=2)
int 0x13 ; 应用 0x13 号 BIOS 中断读入 2 个 setup.s扇区
jnc ok_load_setup ; 读入成功,跳转到 ok_load_setup: ok - continue
mov dx, #0x0000 ; 软驱、软盘有问题才会执行到这里。我们的镜像文件比它们可靠多了
mov ax, #0x0000 ; 复位软驱 reset the diskette
int 0x13 ; 继续尝试加载setup.s模块
jmp load_setup
; 接下来要干什么?当然是跳到 setup 执行。要注意:我们没有将 bootsect 移到 0x9000,因此跳转后的段地址应该是 0x7ce0, 即我们要设置 SETUPSEG=0x07e0
ok_load_setup:
jmpi 0, SETUPSEG
msg1: .byte 13,10
.ascii "Hello OS world, my name is JXL"
.byte 13,10,13,10
.org 510
boot_flag: .word 0xAA55
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
然后我们再次编译,setup.s 和 bootsect.s 都要编译和链接,效率较低,类似我只之前讲到的写个 shell 脚本来编译,这次我们不用自己写,用 Makefile 里自带的即可
$ cd ~/oslab/linux-0.11/
$ make BootImage
as86 -0 -a -o boot/bootsect.o boot/bootsect.s
ld86 -0 -s -o boot/bootsect boot/bootsect.o
as86 -0 -a -o boot/setup.o boot/setup.s
ld86 -0 -s -o boot/setup boot/setup.o
tools/build boot/bootsect boot/setup none > Image
Root device is (3, 1)
Boot sector 512 bytes.
Setup is 512 bytes.
Unable to open 'system'
make: *** [Makefile:54:BootImage] 错误 1
2
3
4
5
6
7
8
9
10
11
12
13
有 Error!这是因为 make 根据 Makefile 的指引执行了 tools/build.c
,它是为生成整个内核的镜像文件而设计的,没考虑我们只需要 bootsect.s
和 setup.s
的情况。它在向我们要 “系统” 的核心代码。为完成实验,接下来给它打个小补丁。
我们可以简单的看看 linux-0.11/Makefile 的内容:
all: Image
Image: boot/bootsect boot/setup tools/system tools/build
cp -f tools/system system.tmp
strip system.tmp
objcopy -O binary -R .note -R .comment system.tmp tools/kernel
tools/build boot/bootsect boot/setup tools/kernel $(ROOT_DEV) > Image
rm system.tmp
rm tools/kernel -f
sync
BootImage: boot/bootsect boot/setup tools/build
tools/build boot/bootsect boot/setup none $(ROOT_DEV) > Image
sync
2
3
4
5
6
7
8
9
10
11
12
13
14
build.c
从命令行参数得到 bootsect、setup 和 system 内核的文件名,将三者做简单的整理后一起写入 Image。其中 system 是第三个参数(argv[3])。当 “make all” 的时候,这个参数传过来的是正确的文件名,build.c
会打开它,将内容写入 Image。
而 “make BootImage” 时,传过来的是字符串 "none"。所以,改造 build.c 的思路就是当 argv[3] 是"none"的时候,只写 bootsect 和 setup,忽略所有与 system 有关的工作,或者在该写 system 的位置都写上 “0”。
修改工作主要集中在 build.c
的尾部,可以参考下面的方式,将圈起来的部分注释掉(也可以删掉)。
当按照前一节所讲的编译方法编译成功后再 run,实验结果如下图
$ cd ~/oslab/linux-0.11
$ make BootImage
$ ../run
2
3
# 获取硬件参数版 v2.0
在理论课里我们讲到过,setup.s 将获得硬件参数放在内存的 0x90000 处。
用 ah=#0x03
调用 0x10
中断可以读出光标的位置,用 ah=#0x88
调用 0x15
中断可以读出内存的大小。有些硬件参数的获取要稍微复杂一些,如磁盘参数表。在 PC 机中 BIOS 设定的中断向量表中 int 0x41
的中断向量位置(4*0x41 = 0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。第二个硬盘的基本参数表入口地址存于 int 0x46
中断向量位置处。每个硬盘参数表有 16 个字节大小。下表给出了硬盘基本参数表的内容:
表 1 磁盘基本参数表
位移 | 大小 | 说明 |
---|---|---|
0x00 | 字 | 柱面数 |
0x02 | 字节 | 磁头数 |
... | ... | ... |
0x0E | 字节 | 每磁道扇区数 |
0x0F | 字节 | 保留 |
所以获得磁盘参数的方法就是复制数据。
下面是将硬件参数取出来放在内存 0x90000 的关键代码。
mov ax,#INITSEG
! 设置 ds = 0x9000
mov ds,ax
mov ah,#0x03
! 读入光标位置
xor bh,bh
! 调用 0x10 中断
int 0x10
! 将光标位置写入 0x90000.
mov [0],dx
! 读入内存大小位置
mov ah,#0x88
int 0x15
mov [2],ax
! 从 0x41 处拷贝 16 个字节(磁盘参数表)
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0004
mov cx,#0x10
! 重复16次
rep
movsb
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
显示获得的参数
现在已经将硬件参数(只包括光标位置、内存大小和硬盘参数,其他硬件参数取出的方法基本相同,此处略去)取出来放在了 0x90000 处,接下来的工作是将这些参数显示在屏幕上。这些参数都是一些无符号整数,所以需要做的主要工作是用汇编程序在屏幕上将这些整数显示出来。
以十六进制方式显示比较简单。这是因为十六进制与二进制有很好的对应关系(每 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 位十六进制的形式显示出来。
下面是完成显示 16 进制数的汇编语言程序的关键代码,其中用到的 BIOS 中断为 INT 0x10,功能号 0x0E(显示一个字符),即 AH=0x0E,AL=要显示字符的 ASCII 码。
! 以 16 进制方式打印栈顶的16位数
print_hex:
! 4 个十六进制数字
mov cx,#4
! 将(bp)所指的值放入 dx 中,如果 bp 是指向栈顶的话
mov dx,(bp)
print_digit:
! 循环以使低 4 比特用上 !! 取 dx 的高 4 比特移到低 4 比特处。
rol dx,#4
! ah = 请求的功能值,al = 半字节(4 个比特)掩码。
mov ax,#0xe0f
! 取 dl 的低 4 比特值。
and al,dl
! 给 al 数字加上十六进制 0x30
add al,#0x30
cmp al,#0x3a
! 是一个不大于十的数字
jl outp
! 是a~f,要多加 7
add al,#0x07
outp:
int 0x10
loop print_digit
ret
! 这里用到了一个 loop 指令;
! 每次执行 loop 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则转移到 loop 指令后的标号处,实现循环;
! 如果为0顺序执行。
!
! 另外还有一个非常相似的指令:rep 指令,
! 每次执行 rep 指令,cx 减 1,然后判断 cx 是否等于 0。
! 如果不为 0 则继续执行 rep 指令后的串操作指令,直到 cx 为 0,实现重复。
! 打印回车换行
print_nl:
! CR
mov ax,#0xe0d
int 0x10
! LF
mov al,#0xa
int 0x10
ret
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
只要在适当的位置调用 print_bx 和 print_nl(注意,一定要设置好栈,才能进行函数调用)就能将获得硬件参数打印到屏幕上,完成此次实验的任务。但事情往往并不总是顺利的,前面的两个实验大多数实验者可能一次就编译调试通过了(这里要提醒大家:编写操作系统的代码一定要认真,因为要调试操作系统并不是一件很方便的事)。但在这个实验中会出现运行结果不对的情况(为什么呢?因为我们给的代码并不是 100% 好用的)。所以接下来要复习一下汇编,并阅读《Bochs 使用手册》,学学在 Bochs 中如何调试操作系统代码。
我想经过漫长而痛苦的调试后,大家一定能兴奋地得到下面的运行结果:
图 4 用可以打印硬件参数的 setup.s 进行引导的结果 Memory Size 是 0x3C00KB,算一算刚好是 15MB(扩展内存),加上 1MB 正好是 16MB,看看 Bochs 配置文件 bochs/bochsrc.bxrc:
!……
megs: 16
!……
ata0-master: type=disk, mode=flat, cylinders=410, heads=16, spt=38
!……
2
3
4
5
这些都和上面打出的参数吻合,表示此次实验是成功的。
实验楼的环境中参数可能跟上面给出的不一致。大家需要根据自己环境中
bochs/bochsrc.bxrc
文件中的内容才能确定具体的输出信息。
下面是提供的参考代码,大家可以根据这个来进行编写代码:
INITSEG = 0x9000
entry _start
_start:
! Print "NOW we are in SETUP"
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#25
mov bx,#0x0007
mov bp,#msg2
mov ax,cs
mov es,ax
mov ax,#0x1301
int 0x10
mov ax,cs
mov es,ax
! init ss:sp
mov ax,#INITSEG
mov ss,ax
mov sp,#0xFF00
! Get Params
mov ax,#INITSEG
mov ds,ax
mov ah,#0x03
xor bh,bh
int 0x10
mov [0],dx
mov ah,#0x88
int 0x15
mov [2],ax
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0004
mov cx,#0x10
rep
movsb
! Be Ready to Print
mov ax,cs
mov es,ax
mov ax,#INITSEG
mov ds,ax
! Cursor Position
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#18
mov bx,#0x0007
mov bp,#msg_cursor
mov ax,#0x1301
int 0x10
mov dx,[0]
call print_hex
! Memory Size
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#14
mov bx,#0x0007
mov bp,#msg_memory
mov ax,#0x1301
int 0x10
mov dx,[2]
call print_hex
! Add KB
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#2
mov bx,#0x0007
mov bp,#msg_kb
mov ax,#0x1301
int 0x10
! Cyles
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#7
mov bx,#0x0007
mov bp,#msg_cyles
mov ax,#0x1301
int 0x10
mov dx,[4]
call print_hex
! Heads
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#8
mov bx,#0x0007
mov bp,#msg_heads
mov ax,#0x1301
int 0x10
mov dx,[6]
call print_hex
! Secotrs
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#10
mov bx,#0x0007
mov bp,#msg_sectors
mov ax,#0x1301
int 0x10
mov dx,[12]
call print_hex
inf_loop:
jmp inf_loop
print_hex:
mov cx,#4
print_digit:
rol dx,#4
mov ax,#0xe0f
and al,dl
add al,#0x30
cmp al,#0x3a
jl outp
add al,#0x07
outp:
int 0x10
loop print_digit
ret
print_nl:
mov ax,#0xe0d ! CR
int 0x10
mov al,#0xa ! LF
int 0x10
ret
msg2:
.byte 13,10
.ascii "NOW we are in SETUP"
.byte 13,10,13,10
msg_cursor:
.byte 13,10
.ascii "Cursor position:"
msg_memory:
.byte 13,10
.ascii "Memory Size:"
msg_cyles:
.byte 13,10
.ascii "Cyls:"
msg_heads:
.byte 13,10
.ascii "Heads:"
msg_sectors:
.byte 13,10
.ascii "Sectors:"
msg_kb:
.ascii "KB"
.org 510
boot_flag:
.word 0xAA55
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
# 扩展
你可以显示不同的字符串,例如修改显示的字符串的颜色等等,或者可以实现载入 system 模块等等,完全可以自己有想做的功能,就做着玩~
可以参考 hoverwinter/HIT-OSLab: S - 哈工大《操作系统》实验 (opens new window),这里实现了 system 模块的载入,并且实验手册也写的很棒
# 附录
# int 0x10 显示服务
AH | 功能 | 调用参数 | 返回参数 |
---|---|---|---|
0x03 | 读光标位置和大小 | BH=页号(0-based) | CH = 光标起始位置, CL = 光标结束位置, DH = 光标行号(0-based), DL = 光标列号(0-based) |
0x0e | 显示字符(光标前移) | AL=字符 BL=前景色 | None |
0x0f | 获取当前显示模式 | None | AL = 当前的显示模式, AH = 屏幕宽度,以字符列, BH = 当前页号(0-based) |
0x1300 | 显示字符串光标留在起始位置 | ES:BP = 字符串地址, CX = 字符串长度, BH = 页号, BL = 显示属性, DH,DL = 显示字符串的起始行号和列号 | None |
0x1301 | 显示字符串光标跟随移动 | ES:BP = 字符串地址, CX = 字符串长度, BH = 页号, BL = 显示属性, DH,DL = 显示字符串的起始行号和列号 | None |