此系列博文用来记录 MOOC 上自学哈工大操作系统课程(由李治军老师授课)时的笔记。
L1
这一节课主要是老师说明学习课程的目的和本课程的要求。另外,就是在实验楼熟悉下实验环境。
L2
这一讲老师主要是在说明电脑接通电源后在做些什么事情。
关于组成计算机的五大部件(这又是老生常谈了):输入设备、输出设备、存储器、运算器、控制器。
计算机的工作方式:取指执行,“指”实际上指的是指针,这个指针指向的是位于内存中的命令。这个概念应该贯穿了整个操作系统的设计。
熟悉一下实验楼的基本操作: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# 解压文件
$ cd oslab
$ tar -zxvf hit-oslab-linux-20110823.tar.gz -C /home/shiyanlou/
# 编译内核
$ cd linux-0.11
$ make all # all 可省略,有时需要先 make clean
# 运行内核
$ cd ~/oslab # 需要再 run 这个脚本文件所在的目录下
$ ./run # 执行这条命令后会出现 Bochs 的窗口
# 汇编调试
$ cd ~/oslab # 同样需要在这个目录下
$ ./dbg-asm
# 可以使用 help 来查看调试系统的命令
# C 语言调试,需要使用两个窗口。
# 第一个窗口
$ cd \~/oslab
$ ./dbg-c
# 第二个窗口
$ cd \~/oslab
$ ./rungdb
# 文件交换
$ cd ~/oslab
$ sudo ./mount-hdc # 先挂载
$ cd hdc
$ ls -l
# 此时看到的文件就是 hdc-0.11.img 这个镜像文件内的文件
$ cd ~/oslab
$ sudo umount hdc # 读写完毕后要卸载
# 1. 注意不要在读写内核文件时运行内核,也不要在运行内核时读写内核内的文件
# 2. 关闭 Bochs 之前,要先执行一下 `sync` 保存一下
L3
这一讲老师主要讲了操作系统启动时做的两件事:
- 读入系统
- 完成初始化
具体如何进行需要借助代码分析。
Experiment 1
实验项目 1 的主题是控制系统的启动,主要包含的内容:
- 阅读《Linux 内核完全注释》的第 6 章,对计算机和 Linux 0.11 的引导过程进行初步的了解;
- 按照下面的要求改写 0.11 的引导程序 bootsect.s
- 有兴趣同学可以做做进入保护模式前的设置程序 setup.s。
改写 bootsect.s 主要完成如下功能:
bootsect.s 能在屏幕上打印一段提示信息“XXX is booting…”,其中 XXX 是你给自己的操作系统起的名字,例如 LZJos、Sunix 等(可以上论坛上秀秀谁的 OS 名字最帅,也可以显示一个特色 logo,以表示自己操作系统的与众不同。)
改写 setup.s 主要完成如下功能:
- bootsect.s 能完成 setup.s 的载入,并跳转到 setup.s 开始地址执行。而 setup.s 向屏幕输出一行”Now we are in SETUP”。
- setup.s 能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
- setup.s 不再加载 Linux 内核,保持上述信息显示在屏幕上即可。
改写 bootsect.s
这个任务比较容易,因为老师上课的时候大致上讲过了,所以只需要按照老师给的提示来完成就行了。
先把实验环境按照上个实验的步骤弄好,解压文件。
先找到 bootsect.s1
2
3$ cd linux-0.11/boot/
$ ls
$ vim bootsect.s
进入 bootsect.s 的编辑页面后能看到 Linus 91年写下的说明,充满了年代感。
映入眼中的全是汇编代码,对于不懂汇编的人来说有点难受。不过没关系,找到指定位置然后修改就行。
首先,要修改的文本的位置是在 bootsect.s 文件内的 244 - 247 行处(直接找就行了):1
2
3
4msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
改为自己喜欢的即可,这里我们改为:color_os is booting ...
。
接着我们还需要在修改一下要显示的字符个数,那么该如何找到代码位置呢?可想而知,这段字符串是要在开机的时候显示的,也就是说,开机后的光标干的第一件事情就是显示这串字符,那只要找到读入光标的位置就行了(其实也可以直接用 vim 的搜索功能找到与 msg1 相关的地方就可以了)。
接下来要修改的位置是在 98 行处:1
mov cx,#24
上面的这个24
实际上就是之前Loading system ...
加上 3 个换行符、3 个回车符的和。这里,数一下修改后的字符个数,应该改为 29
。
这样就差不多了,来尝试编译运行一下:1
2$ cd linux-0.11
$ make all
没有错误提示就可以尝试运行了。1
2$ cd ~/oslab
$ ./run
观察结果可以发现已经改成想要的结果了。
Review
这里我们按照老师给的提示在完成一下这个实验。
根据老师的提示,可以写出 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
24
25
26
27
28
29entry _start
_start:
! read cursor pos
mov ah,#0x03 ! ah need to be set as 03, so the bios interruption can check it
xor bh,bh
int 0x10
! print the message we set
mov cx,#29
mov bx,#0x0007
mov bp,#msg1
mov ax,#0x07c0
mov es,ax ! es need to be set
mov ax,#0x1301
int 0x10
inf_loop:
jmp inf_loop
msg1:
.byte 13,10
.ascii "color_os is booting ..."
.byte 13,10,13,10
.org 510
boot_flag:
.word 0xAA55
这里再来简单解释一下这段汇编代码(毕竟没学过汇编,就当学汇编了),entry
是汇编代码中的伪指令,用来指示汇编程序的入口,显然,在上述代码中,它告诉 cpu 程序的入口是 _start
。!
后面的内容就是注释,mov
、xor
这样的“标识符”在汇编里面叫做操作码,后面紧跟的就是操作数,二者之间用空格隔开,操作数之间用,
隔开。
bios 0x10 中断根据 ah 的不同的值来发挥不同的功能(其他 bios 中断可能也有)。不同功能需要的输入与返回都是不想同的,这部分的内容的疑问可以查询老师给的手册上的注释,也可以直接查 bios 中断的手册。
之所以要改动寄存器 es 的值,是因为 0x10 中断规定了 es:bp 是字符串的首地址(也就是起始位置),也就是说,只有 bp 的值是无法让机器显示字符串的。
info_loop
是利用 jmp
指令设置的一个循环,这条指令会让机器一直执行这个循环。如果没有这条指令,机器就会去寻找下一个能启动的设备。
.org
也是汇编中的伪指令,它告诉 cpu 下面的语句从地址 510 处开始执行,相比原本的内核代码,这里舍弃掉了root_dev
,所以需要将地址设置为 510,这样当我们将磁盘引导扇区(共 512 字节)的最后两个字节设置为 0xAA55 时,机器读取到这里就会知道这个扇区是引导扇区了(是如何读的,这里就不解释了)。最后的 boot_flag
就是启动标志的字面意思了,可想而知,后面跟的就是 0xAA55。可以猜测一下,这几行代码应该是后续工作做的准备。
搞定源码后,就可以开始编译内核运行检查一下运行结果了。以 Linux 为例,进入linux-0.11/boot
目录下要编译和链接 bootsect.s 就要执行下面的命令:1
2as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
别急着去运行内核,先用ls -l
命令检查下编译好的文件的大小。可以发现,bootsect 的文件大小是 544 字节,但是引导程序必须要正好占用一个磁盘扇区,即 512 字节。造成多了 32 个字节的原因是 ld86 产生的是 Minix 可执行文件格式,这样的可执行文件除了文本段、数据段等部分以外,还包括一个 Minix 可执行文件头部,而这个文件的头部正好多了 32 个字节。
在 linux 下需要将文件大小改为 512 字节,也就是删掉头部,所以需要借助命令:1
2dd bs=1 if=bootsect of=Image skip=32
cp ./Image ../Image
之后在 oslab 目录下,运行 run 脚本:1
$ ./run
就可以得到这样的结果:
改写 setup.s
先分析一下要求:
- 利用 setup.s 向屏幕输出一行“Now we are in SETUP”
- 利用 setup.s 获取基本硬件参数,这个内核本身就已经读了内存数
- setup.s 不再加载 linux 内核,保持上述信息显示在屏幕上即可,这里可能会用到中断
先看第一条要求,这个跟前一个任务完成得事情是类似的,需要找到光标位置,然后打印即可。
尝试了一下,将 bootsect.s 内与光标和打印文字相关的代码抄到了 setup.s 这个文件对应读光标的位置,结果并不顶用。突然想到,是不是打印完一行字符后再将光标位置移动到行首呢?于是又在抄过来的代码下面补上了读光标位置的代码,结果还是不行,果然,不懂汇编,单纯的靠 Ctrl + C/V,还是不行,索性看下老师给的提示。
看了老师的提示,才发现老师讲的“改写”跟我理解的“改写”含义不是一样的。老师是直接从新写一个能完成任务的 bootsect.s 和 setup.s 文件,而我是直接改现成文件,水平过低,过低,2333。
不过回过头来想一下,如果只是单纯完成实验,那么确实不需要读取那么多系统信息,也不需要系统完全启动,只要显示了需要的东西即可。既要显示文本,又要内核完好的启动,反而需要的知识量更多,这对初学者而言反而是不利的。好吧,理直气壮的安慰了自己,下面在按照老师给的思路来做一下。
根据老师给出的思路,可以很容易的完成这个实验的第一个任务,具体请看上文 Review。
下面我们首先让 setup.s 完成第一个子任务:向屏幕输出“Now we are in SETUP”。这个任务与 bootsect.s 干的事情很类似,事实上,我们还真的就只需要将前面下好的 bootsect.s 抄过来再修改一下就好了,改好后:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21entry _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
因为 es 的值在 bootsect.s 已经改过了,所以这里要改回来,直接借助 cs 这个段寄存器。至于为什么能借助 cs 段寄存器达到我们的目的,是因为 cs 是保存微处理器执行代码的内存段,cs(代码段寄存器)持有段的起始位置,也就是我们需要的 0x07c0。这里,也不得不说,怪不得 cpu 里面要搞这么多寄存器的,其实都是用的着的,就是着实难记😓。还有一个需要注意的地方,就是 bootsect.s 已经用过 msg1 这个标识符了,这里需要用 msg2 了。
在编译 setup.s 之前,还需要做一件事情,那就是让 bootsect.s 读入 setup.s。那么如何读入 setup.s 呢?其实就是使用 0x13 中断,可以发现到目前位置我们都是通过 bios 中断在达到我们想要的结果。现在看来,bios 中断就像是可供使用的一个工具一样(事实上,它本身就是如此)。
那么跟着老师的思路,我们可以得到修改后的 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
24
25
26
27
28
29
30
31
32
33
34SETUPLEN=2
SETUPSEG=0x07e0
entry _start
_start:
mov ah,#0x03
xor bh,bh
int 0x10
mov cx,#29
mov bx,#0x0007
mov bp,#msg1
mov ax,#0x07c0
mov es,ax
mov ax,#0x1301
int 0x10
load_setup:
mov dx,#0x0000
mov cx,#0x0002
mov bx,#0x0200
mov ax,#0x0200+SETUPLEN
int 0x13
jnc ok_load_setup
mov dx,#0x0000
mov ax,#0x0000
int 0x13
jmp load_setup
ok_load_setup:
jmpi 0,SETUPSEG
msg1:
.byte 13,10
.ascii "color_os is booting ..."
.byte 13,10,13,10
.org 510
boot_flag:
.word 0xAA55
再来简单解释一下这段代码,0x13 中断对 ah、al、bh、bl、ch 和 cl 的值有要求,所以在调用它之前,要先得到我们想要的值。在 linux-0.11 的 bootsect.s 文件中,SETUPLEN 的值为 4,代表了 4 个扇区,这里我们只读 2 个,所以 SETUPLEN 的值在开头设置为 2。jmp load_setup
是在末尾设置的循环,如果载入失败就会从头开始。
载入成功后,会跳转执行 setup.s。此时,需要先搞清楚 setup.s 在哪里,通过前面调用 0x13 中断可知我们将 setup.s 放在了 0x07c0 的后 2 个扇区。而 bootsect.s 本身占 1 个扇区,所以 SETUPSEG 需要设置为 0x07e0,就是 0x07c0 加了 512 字节(1 个扇区)后的地址,这样就完成了从 bootsect.s 到 setup.s 的跳转。
源码完成后就可以编译运行检查结果了。但现在有 2 个文件需要编译、链接。如果都手动编译,就太慢了,所以借助 Makefile 是最佳方式。
进入linux-0.11
目录后,使用下面命令:1
make BootImage
此时会看到下面的错误:1
2Unable to open 'system'
make: *** [BootImage] Error 1
有 Error!这是因为 make 根据 Makefile 的指引执行了tools/build.c
,build.c 是为生成整个内核的镜像文件而设计的,而我们却只需要编译 bootsect.s 和 setup.s 。它在向我们要 “系统” 的核心代码。为完成实验,需要给它打个小补丁。
build.c 的工作原理从命令行参数得到 bootsect、setup 和 system 内核的文件名,将三者做简单的整理后一起写入 Image。其中 system 是第三个参数(argv[3]?)。当make all
或者makeall
的时候,这个参数传过来的是正确的文件名,build.c 会打开它,将内容写入 Image。而make BootImage
时,传过来的是字符串 none。所以,改造 build.c 的思路就是当 argv[3] 是 none 的时候,只写 bootsect 和 setup,忽略所有与 system 有关的工作,或者在该写 system 的位置都写上 “0”。
要达到上述效果,我们只需要将 build.c 中第 178 - 190 行注释掉即可。
然后再进入到~/oslab/linux-0.11
目录下,执行下面的命令:1
2make BootImage
../run
就可以得到下面的结果:
可以发现这个结果跟之前 bootsect.s 产生的结果是类似的,原因就是因为这里的 setup.s 完全就是复制的 bootsect.s。
现在,再来解决最后一个任务:利用 setup.s 获取硬件参数,保存并输出到屏幕上,且不再加载内核。