目录
0. 回顾总结
上一篇文章中我们实现了课程设计2中任务程序相关的所有逻辑,并展示了其中可在虚拟8086模式的DOS下运行的部分,具体功能为:
- 主菜单列出功能选项,让用户通过键盘进行选择,界面如下:
- reset pc ;重新启动计算机
- start system ;引导现有的操作系统
- clock ;进入时钟程序
- set clock ;设置时间
- 用户输入 “1” 后
重新启动计算机。(暂表现为关闭虚拟DOS) - 用户输入 “2” 后
引导现有的操作系统。(虚拟模式中无法与硬盘交互) - 用户输入 “3” 后,执行动态显示当前日期、时间的程序。
- 显示格式如下: 年/月/日 时:分:秒 ;
- 进入此项功能后,一直动态显示当前的时间,在屏幕上将出现时间按秒变化的效果;
- 当按下 F1 键后,改变显示颜色;按下 Esc 键后,返回到主菜单。
- 用户输入 “4” 后可
更改当前的日期、时间,更改后返回到主菜单。(虚拟模式中无法修改软盘数据)
第一部分指路☞:课程设计2 详细实现 (一、虚拟模式篇)
本部分我们将会在虚拟机的实模式DOS中完成剩下的内容。
1. 任务概述
编写一个可以自行启动计算机,不需要在现有操作系统中运行的程序。
提示:
- 在 DOS 下编写安装程序,在安装程序中包含任务程序;
- 运行安装程序,将任务程序写到软盘上;
- 若要任务程序可以在开机后自行执行,要将它写到软盘的0道0面1扇区上。如果程序长度大于512个字节,则需要用多个扇区存放,这种情况下,处于软盘0道0面1扇区中的程序就必须负责将其它扇区中的内容读入内存。
任务总结:将我们在第一部分所编写的任务程序以某种方式写入软盘,使得开机后CPU能够自动从软盘中获取任务程序并加载入内存,实现以任务程序中的逻辑替代原本开机后的初始化操作(最终效果为开机后自动进入我们的主菜单界面)。
环境需求:实模式DOS
2. 环境配置
首先我们需要安装实模式DOS,这里我推荐另一位博主写的一篇文章,其中逐步详细介绍了如何安装虚拟机与配置环境及FreeDos的安装与调试方法:奶酪博士 ——【汇编语言】 安装虚拟机运行dos系统 教程
在这里对这位博主的分享表示感谢!正如他所言,《汇编语言》这本书编写时间较早,其中所需的编程环境如今已经很难还原,我也是踩过许多坑后根据这篇文章才搭建好的环境。
该文章步骤详细、图文并茂、操作简单,并且其中所配置环境完全能够满足本次任务需求,因此本文便不再对其中内容进行过多赘述。
2.1. 在FreeDos中运行任务程序
根据该文章搭建好环境,我们在VMware虚拟机中运行FreeDos,并且通过WinImage制作软盘镜像文件(.flp),这里仅对WinImage的使用方法稍做补充:
- 首先我们进入 WinImage文件夹,打开 winimage.exe
- 事先在DosBox中先将 .asm 文件编译连接为 .exe 文件,再将 .exe 文件拖进来
- 选择默认的1.44 MB
- 保存镜像文件,选择后缀 .flp
- 进入虚拟机设置,让软盘B映像文件关联到刚刚创建的 .flp 文件
- 最后启动虚拟机,运行B盘中的test.exe
可以看到,我们之前编写的任务程序能够在虚拟机中成功运行:
2.2. 虚拟机快照
关于程序的测试我们稍后再进行,现在让我们先认识一个实用的虚拟机功能:虚拟机快照。
- 作用:记录当前虚拟机状态,之后可以随时回溯到当前状态,用于当系统数据遭到破坏时进行恢复。
- 使用原因:我们的程序中涉及到对硬盘和软盘的直接操作,特别是安装程序,需要将任务程序加载到软盘中,这会覆盖软盘中原有执行开机初始化程序的数据。如果我们不使用快照回溯功能,将很难回到初始状态。
了解了这个功能以后,我们来设置一份快照:
- 首先将A盘的初始化文件(BOOT.img)复制一份副本
我们在修改软盘数据时实际上就是在修改软盘映射文件中的数据,因此我们需要复制一份临时文件作为映射对象,保留一份存有原始数据的文件
- 回到虚拟机,确保当前虚拟机处于最佳状态(系统数据未被破坏),并且A盘已经初始化。
- 打开虚拟机设置,将A盘的映像文件关联到刚刚复制的副本.img文件
- 创建一个软盘B(可以关联到一个常用的 .flp 文件)
- 在虚拟机关闭的情况下拍摄快照(左侧图标)
随便起个名字,描述可以不写
来到快照管理器(右侧图标),可以看到我们记录了一个状态,这说明我们以后随时可以通过恢复快照(中间图标)回到这个状态
最后提醒一点:当遇到软存映射文件被破坏时,仅仅使用恢复快照是不够的,我们还需要把映射文件关联到正确的数据文件,即删除原来的副本文件,再复制一份新的副本文件(由于副本文件名字与地址相同,因此我们不需要到虚拟机中重新关联软盘的映射文件)
3. 程序测试
关于环境配置的操作已经完成,接下来我们继续测试任务程序:
3.1. 功能1:重启计算机
- 功能实现 √
3.2. 功能2:引导现有操作系统
- 功能实现失败 ×
不用担心,这是因为硬盘安全系数比较高,等我们将程序安装到初始程序段后便能与之交互
3.3. 功能3:显示时钟
- 时钟动态显示功能实现 √
- 改变颜色功能实现 √
- 返回主菜单功能实现 √
3.4. 功能4:设置时钟
进入子程序4输入数字:
- 字符串输入功能实现 √
Enter键完成输入回到主菜单,然后来到子程序3:
- 修改时钟功能实现 √
重启虚拟机,进入子程序3,还是修改后的时间,再次证明CMOS中的时间数据确实被修改了
提示:由于日期与时间的数字范围限制,有时候时钟并不会按照设置的字符串显示,尽量设置合理的数字
4. 程序安装
4.1. 功能分配
现在让我们思考如何实现开机后自动执行任务程序。
再次阅读以下材料:
开机后,CPU自动进入到 FFFF:0 单元处执行,此处有一条跳转指令。CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
硬件系统检测和初始化完成后,调用 int 19H 进行操作系统的引导。
如果设为从软盘启动操作系统,则 int 19H 将主要完成以下工作。
(1)控制0号软驱,读取软盘0道0面1扇区的内容到 0:7c00;
(2)将 CS:IP 指向 0:7c00。
若要任务程序可以在开机后自行执行,要将它写到软盘的0道0面1扇区上。如果程序长度大于512个字节(一个扇区的容量),则需要用多个扇区存放,这种情况下,处于软盘0道0面1扇区中的程序就必须负责将其它扇区中的内容读入内存。
假设我们的任务程序长度大于一个扇区的容量,此时软盘0道0面1扇区中应当存放一个引导程序,而真正的任务程序则应该安装在其它扇区中,由引导程序来读入内存。
因此,完整的流程是:开机后,CPU执行硬件系统检测和初始化,由 int 19H 读取软盘0道0面1扇区的引导程序到 0:7c00H,将 CS:IP 指向 0:7c00H 执行引导程序,由引导程序从其它扇区中将任务程序读入内存某处,再将 CS:IP 指向该地址执行任务程序。
程序类型 | 安装位置 | 执行功能 |
安装程序 | 无 | 将引导程序和任务程序安装到软盘 |
引导程序 | 软盘0道0面1扇区 | 将任务程序载入内存,跳转到任务程序 |
任务程序 | 其它扇区 | 执行任务 |
- 由于这里任务程序长度小于两个扇区的容量,所以可以使用软盘0道0面2扇区开始的2个扇区进行存储
- 0:7c00H处加载软盘0道0面1扇区的内容,长度为512(200H)字节,计算出末地址为0:7dffH,额外保留256(100H)字节给栈空间,因此任务程序可以被加载到0:7f00H处
代码框架如下:
assume cs:code
code segment
start:
;将引导程序写入软盘0道0面1扇区
;将任务程序写入软盘0道0面2扇区开始的2个扇区
mov ax,4c00H
int 21H
lead: ;引导程序,被保存在软盘0道0面1扇区,由操作系统加载到 0:7c00H
;从0道0面2扇区开始的2个扇区加载主程序到 0:7f00H
;跳转到 0:7f00H
main: ;主程序,被保存在软盘0道0面2扇区开始的2个扇区,由引导程序加载到 0:7f00H
;执行任务
code ends
end start
这里多次涉及到磁盘读写操作,对此不熟悉的可以复习一下第17.4章
4.2. 安装程序
assume cs:code
code segment
start:
mov bx,cs
⭐ mov es,bx
mov bx,offset lead ;将引导程序写入软盘0道0面1扇区
mov al,1 ;操作扇区数量
mov ah,3 ;写入操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,1 ;扇区号
int 13H
mov bx,offset main ;将任务程序写入软盘0道0面2扇区开始的2个扇区
mov al,2 ;操作扇区数量
mov ah,3 ;写入操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,2 ;扇区号
int 13H
mov ax,4c00H
int 21H
lead: ;引导程序,被保存在软盘0道0面1扇区,由操作系统加载到 0:7c00H 处,负责被加载后从0道0面2扇区开始的2个扇区加载主程序
main: ;主程序,被保存在软盘0道0面2扇区开始的2个扇区,由引导程序加载到 0:7f00H
code ends
end start
标⭐处小细节:
为了代码简洁,我们没有申请安全的栈空间,暂时不使用
push cs
pop es
4.3. 引导程序
lead: ;引导程序,被保存在软盘0道0面1扇区,由操作系统加载到 0:7c00H 处,负责被加载后从0道0面2扇区开始的2个扇区加载主程序
sub bx,bx
mov ss,bx
⭐ mov sp,7f00H ;0:7e00H到0:7f00H是安全的栈空间
push cs
pop es
mov bx,7f00H ;将主程序加载到 0:7f00H 处
mov al,2 ;操作扇区数量
mov ah,2 ;读取操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,2 ;扇区号
int 13H
sub bx,bx
push bx
mov bx,7f00H
push bx
retf ;跳转到 0:7f00H 处开始执行主程序
注意标⭐处栈空间的使用
4.4. 任务程序
现在我们已经确定了任务程序最终会被加载到0:7f00H处,考虑到程序中使用到数据标号,应在编译前事先指定编译地址防止地址错乱,具体原因可以参考 实验16 数据标号与org指令
org 7f00H ;防止数据标号错乱
main: ;主程序,被保存在软盘0道0面2扇区开始的2个扇区,由引导程序加载到 0:7f00H
5. 完整测试
完整代码如下:
assume cs:code
code segment
start:
mov bx,cs
mov es,bx
mov bx,offset lead ;将引导程序写入软盘0道0面1扇区
mov al,1 ;操作扇区数量
mov ah,3 ;写入操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,1 ;扇区号
int 13H
mov bx,offset main ;将主程序写入软盘0道0面2扇区开始的2个扇区
mov al,2 ;操作扇区数量
mov ah,3 ;写入操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,2 ;扇区号
int 13H
mov ax,4c00H
int 21H
lead: ;引导程序,被保存在软盘0道0面1扇区,由操作系统加载到 0:7c00H 处,负责被加载后从0道0面2扇区开始的2个扇区加载主程序
sub bx,bx
mov ss,bx
mov sp,7f00H ;0:7e00H到0:7f00H是安全的栈空间
push cs
pop es
mov bx,7f00H ;将主程序加载到 0:7f00H 处
mov al,2 ;操作扇区数量
mov ah,2 ;读取操作
mov dl,0 ;驱动器号 软驱A
mov dh,0 ;面号
mov ch,0 ;磁道号
mov cl,2 ;扇区号
int 13H
sub bx,bx
push bx
mov bx,7f00H
push bx
retf ;跳转到 0:7f00H 处开始执行主程序
org 7f00H ;防止数据标号错乱
main: ;主程序,被保存在软盘0道0面2扇区开始的2个扇区,由引导程序加载到 0:7f00H
push cs
pop ds ;数据标号是按代码段计算的,将数据段与代码段对齐
mov ax,0b800H
mov es,ax ;es固定指向显存段
mov dh,70h ;白底黑字
jmp short main_menu
table dw reset_pc,start_system,show_clock,set_time ;子程序入口定址表
main_menu: ;主菜单
call screen_clear ;清屏
call menu_show ;显示菜单
sub ah,ah
int 16H ;等待输入
;ah返回键盘扫描码,al返回字符ASCII码
cmp ah,1 ;Esc的扫描码
je sret ;为了方便测试,我们令 输入Esc结束程序
cmp al,'1'
jb short main_menu ;无效输入
cmp al,'4'
ja short main_menu ;无效输入
sub bh,bh
mov bl,al
sub bl,'1' ;直接从字符'1'的ASCII码上计算偏移量
shl bx,1
call screen_clear ;跳转子程序前先清屏
call word ptr table[bx] ;选择跳转子程序
jmp short main_menu ;子程序结束回到主菜单选择
sret:
mov ax,4c00H
int 21H
reset_pc: ;重启
mov bx,0ffffH
push bx
sub bx,bx
push bx
retf ;跳转 ffff:0000H
start_system: ;引导现有操作系统
sub bx,bx
mov es,bx
mov bx,7c00H
mov al,1 ;操作扇区数量
mov ah,2 ;读取操作
mov dl,80H ;驱动器号 硬盘C
mov dh,0 ;面号 0
mov ch,0 ;磁道号 0
mov cl,1 ;扇区号 1
int 13H
sub bx,bx
push bx
mov bx,7c00H
push bx
retf ;跳转 0000:7c00H
timestr: db 0,0,'/',0,0,'/',0,0,' ',0,0,':',0,0,':',0,0 ;时间字符串
CMOS_adr db 9,8,7,4,2,0 ;端口地址定址表
show_clock: ;时间显示
push ax
push bx
push cx
push dx
push si
show_clock_run: ;将压栈部分放在重复运行段外面
sub bx,bx ;作为端口地址表的偏移量
mov si,offset timestr ;这里使用地址标号是因为si不是字节类型
mov cx,6
get_CMOS_s:
mov al,CMOS_adr[bx]
call get_CMOS ;在 ds:si 处写入CMOS在al地址处的数据
inc bx
add si,3
loop get_CMOS_s
mov si,offset timestr ;重新指向字符串开头
mov ah,12 ;显示在第12行
mov cx,17 ;时间字符串长度为17字节
call mid_showstr ;居中显示
mov ah,1
int 16H ;int 16H 的 1 号功能:用来查询键盘缓冲区,对键盘扫描但不等待,并设置 ZF 标志位(0有输入,1无输入)
je short show_clock_run ;无键盘输入,继续时钟模式
sub ah,ah
int 16H ;当且仅当有键盘输入,使用 int 16H 的 0 号功能获取输入信息
cmp ah,1 ;Esc的扫描码
je short show_clock_ret ;当输入为Esc,退出时钟模式
cmp ah,3bH ;F1的扫描码
;cmp ah,1cH ;Enter的扫描码
jne short show_clock_run ;其它为无效输入,不进行操作,继续时钟模式
inc dh ;dh保存显示属性,当输入为F1,改变显示属性
jmp short show_clock_run ;循环执行读取CMOS与显示
show_clock_ret:
pop si
pop dx
pop cx
pop bx
pop ax
ret
;参数:al传端口地址,ds:si 传写入地址
get_CMOS: ;从端口获取时间数据并写入 ds:si 处
out 70H,al ;操作单元地址送入地址端口70H
in al,71H ;将该单元从数据端口71H中读取到al
mov ah,al
and ah,1111b ;ah存时间数据低位(个位)
shr al,1
shr al,1
shr al,1
shr al,1 ;al存时间数据高位(十位)
add ax,3030H ;转化为ASCII码
mov [si],ax
ret
clock_setstr: db 12 dup('0') ;时间设置字符串
set_time: ;设置时间
push ax
push cx
push si
mov si,offset clock_setstr ;指向时间设置字符串
sub cx,cx ;字符串初始长度为0
input_char: ;输入字符
mov ah,12 ;显示在第12行
call mid_showstr ;打印字符串
sub ah,ah
int 16H ;监听键盘输入
cmp al,'0'
jb short not_digit
cmp al,'9'
ja short not_digit ;判断是否为数字
call char_push ;添加字符并显示
jmp short input_char ;下一轮输入
not_digit:
cmp ah,0eH ;撤回键
je short backspace
cmp ah,1cH ;回车键
je short enter
jmp short input_char ;非法键,下一轮输入
backspace:
call char_pop ;撤销字符并显示
jmp short input_char ;下一轮输入
enter: ;结束字符串输入,修改CMOS并返回主菜单
push bx
sub bx,bx ;bx记录 CMOS_adr 偏移量
mov cx,6
set_CMOS_s:
push bx
mov al,CMOS_adr[bx] ;可以复用 CMOS_adr 数据标号
mov bx,[si]
call set_CMOS ;在CMOS的al地址处写入bx数据
pop bx
inc bx
add si,2
loop set_CMOS_s
pop bx
pop si
pop cx
pop ax
ret
;bx传时间ASCII码(bl存十位,bh存个位),al传端口号
set_CMOS: ;修改CMOS
push bx
out 70H,al
sub bx,3030H ;ASCII码转BCD码
shl bl,1
shl bl,1
shl bl,1
shl bl,1 ;bl高位存十位
or bl,bh ;bl低位存个位
mov al,bl
out 71H,al ;通过数据端口写入CMOS
pop bx
ret
;参数:al入栈字符,字符串长度首地址 ds:si,长度 cx
;返回值:字符串新长度 cx
char_push: ;字符入栈,指针后移,最多12位
cmp cx,12
je short char_push_ret ;最多12位
push bx
mov bx,cx
mov [bx][si],al ;寻找当前指针所指向字符
inc cx
pop bx
char_push_ret:
ret
;字符串长度首地址 ds:si,长度 cx
;返回值:al返回出栈字符,字符串新长度 cx
char_pop: ;字符出栈,指针前移,至少0位
cmp cx,0
je short char_pop_ret ;至少0位
push bx
mov bx,cx
mov al,[bx][si] ;寻找当前指针所指向字符
dec cx
pop bx
char_pop_ret:
ret
;参数:dh传入颜色属性
screen_clear: ;清屏
push bx
push cx
push dx
sub bx,bx
mov dl,' ' ;dl 传入字符' ',dh 传入颜色属性
mov cx,2000
clears:
mov es:[bx],dx
add bx,2
loop clears
pop dx
pop cx
pop bx
ret
;参数:行数ah,字符串首地址为 ds:si,长度为cx,最大为80,dh传入颜色属性
mid_showstr: ;字符串居行中显示
push ax
push cx
push si
push di
mov al,160
mul ah ;行数
mov di,80
sub di,cx
add di,ax ;根据字符串长度计算显示位置
shr di,1
shl di,1 ;对齐偶数单元地址
cld
mov byte ptr es:[di-2],' ' ;将字符串前一格置空格,至少保证执行一次,以清除最后一个字符
jcxz short showstr_ret ;如果长度为0就不用显示
showstr_s:
movsb
mov es:[di],dh
inc di
loop showstr_s
mov byte ptr es:[di],' ' ;将字符串后一格置空格
showstr_ret:
pop di
pop si
pop cx
pop ax
ret
line1 db "Press (1) - RESET PC"
line2 db "Press (2) - START SYSTEM"
line3 db "Press (3) - SHOW CLOCK"
line4 db "Press (4) - SET TIME"
lines dw line1,line2,line3,line4
lengths db line2-line1,line3-line2,line4-line3,lines-line4
;参数:通过dh传入字符颜色属性
menu_show: ;主菜单显示
push ax
push cx
push si
mov ah,15 ;将四个字符串分别显示到9、11、13、15行
mov cx,4
menu_show_s:
push cx ;调用 mid_showstr 前会修改cl,先把循环次数保存起来
mov si,cx
dec si ;通过循环次数计算偏移量
mov cl,lengths[si] ;lengths为字节型数据组,使用cl保存长度
shl si,1 ;注意数据类型,偏移量乘2
mov si,lines[si] ;lines为字型数据组,使用si保存字符串首地址
call mid_showstr
sub ah,2 ;自下而上
pop cx
loop menu_show_s
pop si
pop cx
pop ax
ret
code ends
end start
在FreeDos中运行该安装程序后,重启虚拟机检验开机效果:
开机后自动进入到我们的主菜单界面 √
其它功能之前已经测试过,重点检查 第2个功能:引导现有操作系统
功能实现 √
温馨提示:
如果这里出现了白板或者其它异常,可能是因为系统数据被破坏,可以使用之前介绍过的重置方法将虚拟机恢复到最佳状态
至此,我们总算是完整完成了《汇编语言》课设2 中所有任务!(^_^) / 撒花~
6. 总结
课程设计2作为《汇编语言》所有课程的总结练习,几乎涵盖了书中所有知识点,唯有将这些知识点真正融会贯通,才能完成这个练习。我们将整个程序分成了两个部分进行实现:任务程序部分与安装程序部分,并且简单了解了实模式DOS的环境搭建与使用。通过与操作系统直接交互能让我们对其工作原理有一个更加深刻的理解,为计算机的学习之路打下基础。也希望大家在今后的学习过程中戒骄戒躁,一步一个脚印,定能来到成功的彼岸!
7. 预告
课程设计的逻辑部分已经全部实现,下一篇文章中我会对部分界面及光标显示进行美化调整,这部分内容属于选看部分,有兴趣的朋友可以来参考一下:课程设计2 详细实现(三、界面优化篇)
参考材料:《汇编语言》(第4版)王爽