操作系统实验一_引导扇区程序
实验目的
锻炼编写汇编语言程序的能力,增加对操作系统启动方式的了解,学习bochs调试工具、NASM编译工具的使用。
实验要求
设计IBM_PC的一个引导扇区程序,程序功能是:用字符‘A’从屏幕左边某行位置45度角下斜射出,保持一个可观察的适当速度直线运动,碰到屏幕的边后产生反射,改变方向运动,如此类推,不断运动;在此基础上,增加你的个性扩展,如同时控制两个运动的轨迹,或炫酷动态变色,个性画面,如此等等,自由不限。还要在屏幕某个区域特别的方式显示你的学号姓名等个人信息。将这个程序的机器码放进放进第三张虚拟软盘的首扇区,并用此软盘引导你的XXXPC,直到成功。
实验环境
使用NASM来编译代码,由于VSCode里可以使用终端,所以我用VSCode来编写汇编代码,然后可以方便的在终端里用NASM。
用《x86汇编语言-从实模式到保护模式》这本书附带的fixvhdwr来将二进制文件写入至硬盘,然后使用bochs2.1.1来对程序进行调试,调试无误后把二进制文件放到VirtualBox运行。
实验过程
准备工作
我计划按实验要求的方法在屏幕上循环显示”reeeeeeeeeein”,并且字符的颜色不相同,每显示一些字符之后就把屏幕刷新。如下图所示:
首先我们要确定当前要显示的位置,这就需要知道是在哪一行哪一列。于是我在程序末尾分配两个变量:
1 | locr: db 0 ;当前所在行 |
为了方便,以后直接用locr指代[ds:locr]这个位置存储的值,其他变量也是同理。
知道了当前位置,还需要知道下一个位置。下一个位置无非是由当前位置往左往右往上往下得到,于是我又分配了两个变量;
1 | u_or_d: 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个颜色:
所以分配一个变量存储当前的颜色:
1 | color: db 8;颜色从8显示到15 |
在我的电脑上,bochs和VirtualBox虚拟机的屏幕宽度都是80个字符。于是我设置可显示的宽度为80,可显示的高度为17。
1 | %define width 80 |
要刷新屏幕,得用一个计数器,当它减到0就刷新一次:
1 | %define flush_seq 300 |
1 | flush_or_not: dw flush_seq;注意这里只能dw不能db,因为300>255 |
此外,为了显示我的””知识产权”,我会屏幕下方显示”made by zjr”这个字符串。
代码解读
显示字符
把上面这些工作做完了,就可以开始编写一些重要的代码了。首先看一下显示字符的代码:
1 | do: |
当前显示的位置是:
$$
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 | tem1: |
先看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 | tem5: |
先判断locr是否等于height-1,不等于就跳到tem6位置,不执行第4行。如果等于就执行第4行,把u_or_d改为1;再判断它是否为0,是的话就执行第9行,把u_or_d改为0。值得注意的是,两个判断语句是相互独立的,所以第三行不能调到tem7,而是要跳到tem6。
第11行往后跟前面基本是一样的。
其他工作
每打印一个字符,会让程序暂停一段时间:
1 | tem9: |
考虑到如果屏幕字符很多会很影响观看体验,所以每打印300个字符会把屏幕刷新一遍:
1 | cmp word [ds:flush_or_not],0 |
当flush_or_not等于0时,把它重新置为flush_seq,然后跳到flush段的代码:
1 | flush: |
首先用width和height相乘,得到要刷新的总字节数。然后执行循环,当ax<bx时继续,否则退出。循环里把di*2,然后将es:di和es:di+1置为0(注意一个字符占的位置是两个字节),最后ax+1,将di置为ax。
把上面这段代码放在do标志前面,这样程序开始的时候就会把屏幕刷新。
在程序开始的地方要设置段寄存器和附加段寄存器:
1 | mov ax,0x7c0 |
在屏幕下方打印”made by zjr”:
1 | mov ax,width |
由于显示”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的虚拟硬盘:
然后选择这个二进制文件:
写入完成后,可以用bochs打开:
可以看到,结果符合预期。
然后可以用VirtualBox验证一下:
首先把刚才生成的二进制文件保存为img格式:
新建一个虚拟机:
点击右边的存储,往里面添加刚才创建的img文件作为软驱:
在系统一栏设置启动顺序为软驱优先:
启动后就能看到结果:
(似乎在virtualbox里显示的比bochs慢,并不知道为什么)
心得体会
这次实验整体难度不算特别大,只对编写汇编代码有较基本的要求。我本来在2月份的时候就已经完成了,但只录了视频,没有写实验报告就交上去了,结果两个月过后,代码找不到了,实验报告也写不成了,没办法,只能重新写一遍,就算是练习吧。
虽然写的代码并不多,但还是有不少奇奇怪怪的bug,比如:
这里loc变量长度是一个字节,而si是一个16位的寄存器。把loc赋值给si后,尽管我加了个byte,但第36行的[ds:si]还是会取两个字节的值。一开始loc后面是没有变量的,运行结果十分正常,后来我在后面加了一个
取[ds:si]的时候就得到一个很大的值。这个bug困扰了我很久,直至我想起来si是个16位寄存器。
类似于这样的bug还有很多,不再一一列举。以后我得多练习汇编语言编程,为以后其他的实验做准备。
本次实验最大的收获还是增强了汇编语言编程能力,此外我还学会了用bochs进行debug,这对以后做实验是一个很大的帮助。还有的就是感谢TA推荐了Typora这个编写MarkDown文档的工具,确实比我之前用的VSCode插件好用很多,也算是一个意外之喜。