0%

操作系统实验一_引导扇区程序

操作系统实验一_引导扇区程序

实验目的

​ 锻炼编写汇编语言程序的能力,增加对操作系统启动方式的了解,学习bochs调试工具、NASM编译工具的使用。

实验要求

​ 设计IBM_PC的一个引导扇区程序,程序功能是:用字符‘A’从屏幕左边某行位置45度角下斜射出,保持一个可观察的适当速度直线运动,碰到屏幕的边后产生反射,改变方向运动,如此类推,不断运动;在此基础上,增加你的个性扩展,如同时控制两个运动的轨迹,或炫酷动态变色,个性画面,如此等等,自由不限。还要在屏幕某个区域特别的方式显示你的学号姓名等个人信息。将这个程序的机器码放进放进第三张虚拟软盘的首扇区,并用此软盘引导你的XXXPC,直到成功。

实验环境

​ 使用NASM来编译代码,由于VSCode里可以使用终端,所以我用VSCode来编写汇编代码,然后可以方便的在终端里用NASM。
​ 用《x86汇编语言-从实模式到保护模式》这本书附带的fixvhdwr来将二进制文件写入至硬盘,然后使用bochs2.1.1来对程序进行调试,调试无误后把二进制文件放到VirtualBox运行。

实验过程

准备工作

​ 我计划按实验要求的方法在屏幕上循环显示”reeeeeeeeeein”,并且字符的颜色不相同,每显示一些字符之后就把屏幕刷新。如下图所示:

image.png

​ 首先我们要确定当前要显示的位置,这就需要知道是在哪一行哪一列。于是我在程序末尾分配两个变量:

1
2
locr: db 0 ;当前所在行
locc: db 0 ;当前所在列

​ 为了方便,以后直接用locr指代[ds:locr]这个位置存储的值,其他变量也是同理。

​ 知道了当前位置,还需要知道下一个位置。下一个位置无非是由当前位置往左往右往上往下得到,于是我又分配了两个变量;

1
2
u_or_d: db 0 ;上还是下,0是下,1是上
l_or_r: db 0 ;0右,1左

​ 要显示”reeeeeeeeeein”,得知道现在要显示的是第几个字符,还要把这个字符串存储起来:

1
words: db 'r','e','e','e','e','e','e','e','e','e','e','i','n'
1
loc: db 0 ;当前显示第几个字母
1
%define name_length 13;字符串的长度为13

​ 为了美观,背景颜色就用默认的黑色,而字符的前景颜色用颜色表里I=1的8个颜色:
image.png
​ 所以分配一个变量存储当前的颜色:

1
color: db 8;颜色从8显示到15

​ 在我的电脑上,bochs和VirtualBox虚拟机的屏幕宽度都是80个字符。于是我设置可显示的宽度为80,可显示的高度为17。

1
2
%define width 80
%define height 17

​ 要刷新屏幕,得用一个计数器,当它减到0就刷新一次:

1
%define flush_seq 300
1
flush_or_not: dw flush_seq;注意这里只能dw不能db,因为300>255

​ 此外,为了显示我的””知识产权”,我会屏幕下方显示”made by zjr”这个字符串。

代码解读

显示字符

​ 把上面这些工作做完了,就可以开始编写一些重要的代码了。首先看一下显示字符的代码:

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
do:
xor ax,ax
mov al,[ds:locr]
imul bx,ax,width
mov ax,bx
xor bx,bx
mov bl,[ds:locc]
add ax,bx
add ax,ax
mov di,ax;到这里,es:di就是我们要访问的位置
xor ax,ax
mov al,[ds:loc]
mov si,ax
mov al,[ds:si+words]
mov [es:di],al
mov al,[ds:color]
mov byte [es:di+1],al
inc al
cmp al,16
jne tem0
mov al,8
tem0:
mov [ds:color],al
inc byte [ds:loc]
cmp byte [ds:loc],name_length
jne tem1
mov byte [ds:loc],0

​ 当前显示的位置是:

$$
locr*width+locc
$$

​ 第二行把ax清0(这是有必要的,如果ah的值不为0可能得到错误的结果)。把locr的内容放到al里,然后第四行把ax和width相乘,结果放到bx。

​ 第五行到第十行是把bx放到ax,并把locc加到ax里,然后ax乘2。ax乘2的原因是显示屏上显示一个字符要2个字节。最后把ax放到di里。这样,es:di就是我们当前要显示的位置。

​ 从第11行到第15行是找到要显示的字符。首先取出loc,再用$ds:words+loc$得到要显示的字符的位置,最后把这个字符取出来,放到$es:di$里。

​ 第16行开始是在确定字符的前景颜色,这跟找到字符是差不多的。注意color要自增一次,自增完之后要判断它是否等于16,如果等于,就把color置为8。

​ 第24行开始是对loc的自增,然后判断是否是8,是的话就置为0。

下一个位置

​ 显示了当前的字符后,要确定下一个字符的位置,这就需要u_or_d和l_or_r这两个变量。u_or_d取值为0,则locr要加1,否则locr要减1;l_or_r取值为0,则locc加1,否则locc要减1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tem1:
cmp byte [ds:u_or_d],0
je tem2
dec byte [ds:locr]
jmp tem3
tem2:
inc byte [ds:locr]
tem3:

cmp byte [ds:l_or_r],0
je tem4
dec byte [ds:locc]
jmp tem5
tem4:
inc byte [ds:locc]

​ 先看1到7行:判断u_or_d是否为0,是则跳到tem2,把locr加1;否则把locr减1,然后跳到tem3,不执行tem2那里的操作。其实就是一个if-else语句。

​ 10到16行基本与之相同,不再赘述。

调整方向

​ 接下来,要判断方向要不要调整。如果locr到达height-1,则要把u_or_d改为1;如果locr到达0,则要把u_or_d改为0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tem5:
cmp byte [ds:locr],height-1
jne tem6
mov byte [ds:u_or_d],1

tem6:
cmp byte [ds:locr],0
jne tem7
mov byte [ds:u_or_d],0

tem7:
cmp byte [ds:locc],width-1
jne tem8
mov byte [ds:l_or_r],1

tem8:
cmp byte [ds:locc],0
jne tem9
mov byte [ds:l_or_r],0

​ 先判断locr是否等于height-1,不等于就跳到tem6位置,不执行第4行。如果等于就执行第4行,把u_or_d改为1;再判断它是否为0,是的话就执行第9行,把u_or_d改为0。值得注意的是,两个判断语句是相互独立的,所以第三行不能调到tem7,而是要跳到tem6。

​ 第11行往后跟前面基本是一样的。

其他工作

​ 每打印一个字符,会让程序暂停一段时间:

1
2
3
4
5
tem9:
mov ah,86h
mov cx,0x1E
mov dx,0x8480
int 15h

​ 考虑到如果屏幕字符很多会很影响观看体验,所以每打印300个字符会把屏幕刷新一遍:

1
2
3
4
5
6
7
cmp word [ds:flush_or_not],0
jne tem10
mov word [ds:flush_or_not],flush_seq
jmp flush
tem10:
dec word [ds:flush_or_not]
jmp do

​ 当flush_or_not等于0时,把它重新置为flush_seq,然后跳到flush段的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
flush:
mov ax,width
imul bx,ax,height
xor ax,ax
mov di,ax
flush_do:
add di,di
mov byte [es:di],0
mov byte [es:di+1],0
inc ax
mov di,ax
cmp ax,bx
jl flush_do

​ 首先用width和height相乘,得到要刷新的总字节数。然后执行循环,当ax<bx时继续,否则退出。循环里把di*2,然后将es:di和es:di+1置为0(注意一个字符占的位置是两个字节),最后ax+1,将di置为ax。

​ 把上面这段代码放在do标志前面,这样程序开始的时候就会把屏幕刷新。

​ 在程序开始的地方要设置段寄存器和附加段寄存器:

1
2
3
4
mov ax,0x7c0
mov ds,ax
mov ax,0xb800
mov es,ax

​ 在屏幕下方打印”made by zjr”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mov ax,width
imul bx,ax,height+2
add bx,bx
mov di,bx
xor cx,cx
mov si,cx
do2:
mov al,[ds:si+my_name]
mov [es:di],al
inc di
inc di
inc si
cmp si,name2_length
jne do2

​ 由于显示”reeeeeeeeeein”的位置是左上角(0,0)到右下角(height-1,width-1),可以让”maded by zjr”字符串在(height+2,0)开始显示,于是用bx存储$width(height+2)2$,把它复制给di,然后就可以用es:di直接访问要存储的位置。此后进入循环,用si做循环变量,每次用[ds:si+my_name]得到要打印的字符,赋值给al,再把它送到到打印的位置。

效果展示

​ 在VSCode里新建终端,输入

1
NASM proj.asm -o p

​ 得到一个二进制文件。然后使用fixvhdwr把它写到一个文件格式为vhd的虚拟硬盘:

image-20200423194702107.png

​ 然后选择这个二进制文件:

image-20200423194811099.png

​ 写入完成后,可以用bochs打开:

image-20200423195008833.png

​ 可以看到,结果符合预期。

​ 然后可以用VirtualBox验证一下:

​ 首先把刚才生成的二进制文件保存为img格式:

image-20200423195208552.png

​ 新建一个虚拟机:

image-20200423195406846.png

​ 点击右边的存储,往里面添加刚才创建的img文件作为软驱:

image-20200423195524820.png

在系统一栏设置启动顺序为软驱优先:

image-20200423195708890.png

​ 启动后就能看到结果:

image-20200423195854503.png

​ (似乎在virtualbox里显示的比bochs慢,并不知道为什么)

心得体会

​ 这次实验整体难度不算特别大,只对编写汇编代码有较基本的要求。我本来在2月份的时候就已经完成了,但只录了视频,没有写实验报告就交上去了,结果两个月过后,代码找不到了,实验报告也写不成了,没办法,只能重新写一遍,就算是练习吧。

​ 虽然写的代码并不多,但还是有不少奇奇怪怪的bug,比如:

image-20200423200349299.png

​ 这里loc变量长度是一个字节,而si是一个16位的寄存器。把loc赋值给si后,尽管我加了个byte,但第36行的[ds:si]还是会取两个字节的值。一开始loc后面是没有变量的,运行结果十分正常,后来我在后面加了一个image-20200423200602504.png

​ 取[ds:si]的时候就得到一个很大的值。这个bug困扰了我很久,直至我想起来si是个16位寄存器。

​ 类似于这样的bug还有很多,不再一一列举。以后我得多练习汇编语言编程,为以后其他的实验做准备。

​ 本次实验最大的收获还是增强了汇编语言编程能力,此外我还学会了用bochs进行debug,这对以后做实验是一个很大的帮助。还有的就是感谢TA推荐了Typora这个编写MarkDown文档的工具,确实比我之前用的VSCode插件好用很多,也算是一个意外之喜。