使用工具
VMware虚拟机
个人使用上 VMware 比 VirtualBox 要稳定很多,特别是 VMware 15。
Windows xp系统
xp 系统使用内置的 ie 不能连接网络,我这里选择火狐浏览器 xp 版本。
对于汇编文本的编辑这里选用 sublime text3 加 x86-assembly-textmate-bundle-master 高亮插件。
适用于本系统的密钥实测有效:YBVJB-YV2JW-7FHPT-6D8XG-RT83G 。
x86高亮插件的安装
点击箭头指示,将资源中 x86-assembly-textmate-bundle-master 解压然后放至该目录下,再点击上图右下角切换为如上图显示的 x86 and x86_64 assembly 主题。
实验流程
在dosbox中的实验
这里推荐先完成 mini_OS 程序的子功能1,4,5,因为这三个功能基本可以在 dosbox 下完成。在每个功能开头都可以清一次屏幕,具体原理参看《汇编语言》实验十六。
列出功能项
由于要列出的字符串比较多,故使用了直接定址表方便直接调用。
strs dw l0,l1,l2,l3,l4,l5,l6,l7,date,date_form
l0 db 'Please choose the following options',0
l1 db '(1) RESET PC',0
l2 db '(2) BOOT OS',0
l3 db '(3) SHOW CLOCK',0
l4 db '(4) SET CLOCK',0
l5 db '---Copyright@kctaig---',0
l6 db 'Enter ESC to return or S to change colors',0
l7 db 'Please enter the date in the following format',0
date db "--/--/-- --:--:--",0
date_form db 9,8,7,4,2,0
table dw str_push,str_pop,str_show
top dw 0
这一功能实现比较简单,主要使用了《汇编语言》中的字符串输出。这里为了后续输出方便,将字符串输出写为一个子程序。代码如下:
;----------------- 字符串输出 ---------------------
;es:di表示屏幕显示位置
;bx表示字符串在strs表中的位置
;cx表示要连续输出的字符串个数
str_output:
push si
push di
add bx,bx ;strs中是dw大小,这里计算偏移量
s0: mov si,strs[bx] ;将定址表strs中第(bx)个的偏移地址传递到si中
s1: cmp byte ptr cs:[si],0
je next_raw ;若等于零则结束该字符串
mov al,cs:[si]
mov es:[di],al
inc si
add di,2
jmp short s1
next_raw:
add bx,2 ;下一个字符串,注意是加2
pop di
add di,160 ;屏幕下一行
push di
loop s0
pop di ;将屏幕显示的位置pop掉
pop si
ret
所以具体功能的实现代码如下:
;------------ (1)列出功能项 ------------
list_show:
call clear ;清屏
push ax
push bx
push cx
push es
push di
mov ax,0b800h
mov es,ax
mov di,160*10+40
mov bx,0
mov cx,1
call str_output ;显示l0
mov di,160*11+60
mov bx,1
mov cx,4
call str_output ;显示l1~l4
mov di,160*15+50
mov bx,5
mov cx,1
call str_output ;显示l5
pop di
pop es
pop cx
pop bx
pop ax
ret
动态显示当前时间
对于时间的显示参考实验《汇编语言》实验十四,这里我自己因为粗心循环调用的时候用的是 call而不是 jmp 导致程序出错浪费了一个下午,希望读者能引以为戒。
当按下 s 键后,改变现实颜色;按下 ESC 键后,返回到主选单。这里改为s键是因为我的 F1 键不管在加不加 Fn 键都无法使用,后来就放弃了,每次操作软键盘太麻烦;而且没有修改 int 9h 也不会对其他功能造成影响。
我参考的是这位仁兄的方法,我原先使用的是修改 int 9h 键盘中断,但是在调试的时候出现问题。这个问题在《汇编语言》实验十五中也出现了,就是在修改中断向量表时 debug 卡死,但是程序本身又能运行。如果非要用 int 9h 可以参考这位的实验。我偶然看到这个博客,但本人未验证过,有兴趣的朋友可以试试。
;-------------- (4)动态显示当前时间 --------------
show_clock:
call clear
call clear_input ;清理键盘缓存区
push ax
push bx
push cx
push di
push ds
push es
mov ax,0b800h
mov es,ax
mov di,160*10+40
mov bx,6
mov cx,1
call str_output ;显示l6字符串
mov ax,1; ;设置颜色
push ax
rep_show:
mov ax,cs
mov ds,ax
mov bx,0
mov ax,0b800h
mov es,ax
mov di,160*12+60 ;es:di输出屏幕位置
mov cx,6
s: push cx
mov al,date_form[bx]
push ax
out 70h,al
in al,71h ;此时从端口中获取到数据
mov ah,al
mov cl,4
shr ah,cl
and al,00001111b
add ah,30h
add al,30h ;此时ah为十位,al为个位
mov es:[di],ah
mov es:[di+2],al ;输出
add di,4
pop ax
cmp al,9
jne ok1
mov byte ptr es:[di],'/'
add di,2
jmp ok
ok1: cmp al,8
jne ok2
mov byte ptr es:[di],'/'
add di,2
jmp ok
ok2: cmp al,7
jne ok3
add di,2
jmp ok
ok3: cmp al,4
jne ok4
mov byte ptr es:[di],':'
add di,2
jmp ok
ok4: cmp al,2
jne ok
mov byte ptr es:[di],':'
add di,2 ;输出间隔符号
ok: inc bx
pop cx
loop s
mov ah,1
int 16h ;利用16h中断的1号功能,非阻塞读取
jz rep_call ;如果无缓存字符,继续循环
mov ah,0
int 16h ;将一号功能读取的字符从缓存中清除
cmp al,1bh ;判段是否是esc
je cret
cmp ah,1fh ;用s键来改变颜色
je change_color
jmp rep_call ;若是其他键则继续显示
change_color:
pop ax
inc ax
cmp ax,7 ;设置颜色数量上线
je reset_col
jmp next_col
reset_col: ;重置颜色
mov ax,1
next_col:
push ax ;将下一个颜色入栈
call set_color
rep_call:
jmp rep_show ;循环显示时间
cret: mov ax,7 ;将前景色设置为白色
call set_color
pop ax ;将颜色出栈
pop es
pop ds
pop di
pop cx
pop bx
pop ax
ret
修改日期
先建立一个字符串 date ,利用《汇编语言》实验第十七章的 17.3 的程序(会根据需求略作修改)将输入的日期写入 date 。
再将 date 中的年月日等提取出来改写为 BCD CMOS 中。
注:在 dosbox 中修改的日期会无法显示,具体效果只能在裸机上看到。(这个坑真的浪费我好长时间,很无语)
get_str 调用十七章的 17.3,由于太长且书上有就不贴这部分代码了,所有代码会贴在最后面。
;--------------- (5)修改日期 ----------------
;先将日期写入date字符串中,保证只输入日期和格式正确
;再将字符串中的数字写入cmos中
set_clock:
call clear ;清除屏幕
push ax
push dx
push si
push di
push ds
push es
mov ax,0b800h
mov es,ax
mov di,160*10+40
mov bx,7
mov cx,1
call str_output ;显示l7字符串
mov di,160*12+64
mov bx,8
mov cx,1
call str_output ;显示日期格式
mov dh,12
mov dl,32 ;设置字符串在屏幕上的行列位置
mov ax,cs
mov ds,ax
mov si,offset date ;ds:si指向字符栈空间
call get_str ;输入新日期
call set_cmos ;将新日期写入cmos中
pop es
pop ds
pop di
pop si
pop dx
pop ax
ret
;--------------- 修改cmos中71端口的内容 ---------------
set_cmos:
push ax
push bx
push cx
push si
mov bx,0
mov cx,6
cmos_s: call get_date_cut ;此时al中为日期
mov ah,al
mov al,date_form[bx]
out 70h,al ;将日期对应地址写入70h端口
mov al,ah
out 71h,al ;修改cmos中的内容
inc bx
loop cmos_s
pop cx
pop bx
pop ax
ret
;-------------- 截取date中年、月、日等 ----------------
;参数bx表示截取的位置
;al存储截取出的日期
get_date_cut:
push bx
push cx
push di
push es
mov al,3
mul bl
mov bl,al ;计算偏移量
mov ax,cs
mov es,ax
mov di,offset date
add di,bx ;es:di指向date字符串中日期的位置
mov ax,es:[di]
sub al,30h
sub ah,30h
mov cl,4
shl al,cl ;输入的时候先输入的十位在al中
and al,11110000b;保留al中的前四位
add al,ah ;al中为两位数日期的的BCD码
pop es
pop di
pop cx
pop bx
ret
以上基本完成了在 dosbox 中完成的 mini_OS 中的操作,在 dobox 中还会涉及 loader 程序(即安装boot 和 mini_OS 到软盘中)。
在裸机中的实验
机器开机后执行流程
1、开机后 CPU 进入到 FFFF:0000单元处执行,此处为一条跳转指令
2、CPU 执行该跳转指令后,转去执行 BIOS 中的硬件系统检测和初始化程序
3、之后调用 int 19h 中断进行操作系统引导,这里从软盘A启动操作系统
4、int 19h 从软盘启动操作系统将完成以下工作
- 控制0号软驱,去读软盘的0道0面1扇区的内容(本实验中即 boot )到 0:7c00h
- 将 cs:ip 指向 0:7c00h
5、本实验中 loader 将 boot 写入 0:7c00h,故此处执行 boot
6、再执行 mini_OS ,一个能自行启动计算机,无需在现有操作系统中运行的程序
本实验三个子程序
loader 程序:在 dosbox 中执行,功能如下:
- 将 boot 写入0道0面1扇区
- 将 mini_OS 写入之后的其他扇区
boot 程序:由 int19h 中断将之加载进入内存 0:7c00h,功能如下:
- 将 mini_OS 从软盘A中加载进内存 0:7e00h
- 将 cs:ip 指向 0:7e00h
mini_OS 程序:小型循环操作系统,功能如下:
- 列出功能项
- 用户输入1后重新启动计算机
- 用户输入2后引导现有操作系统( c 盘中 win xp)
- 用户输入3后动态显示当前时间
- 用户输入4后修改日期
注:用 loader 与 boot 的目的是能够扩展引导启动程序。因为 int 19h 只能读取一个扇区,而boot 可以占据多个之后的扇区。
软盘处理
这里参考了在VMWare中软盘的安装。
个人觉得这里是实验最难顶的部分,因为软盘启动黑屏无法调试,但是软盘中确实写入了 boot 和 init_OS程序。而VMware 中不好调试裸机;VirtualBox 中虽然有内置调试工具,但我用了发现无法调试引导程序,也就是 Boot 程序;Bochs 需要用 NASM ,并且对于以及写好的软盘在 Windows 写 bochsrc配置文件我没有发现中文文档有具体的方法(看了一圈都是讲bin写进 img 中生成镜像,而软盘本身就可以自己当作启动盘,如果改程序变为 NASM 格式也很麻烦。),当我打算看 Bochs 原文档时,修改了一下返回指令成功了,原因后面会详细说的。总之,这里对于调试非常不友好,在没有人带的情况下很难顶。
安装程序
即 loader,目的是将 boot(系统引导程序)放入软盘0道0面1扇区,并且将 mini_OS 放入之后的3个扇区。具体代码如下:
;------------- loader程序 --------------
loader: mov ax,cs
mov es,ax
mov bx,offset boot
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,3
int 13h ;将boot写入软盘0道0面1扇区
mov bx,offset mini_OS
mov al,3
mov ch,0
mov cl,2
mov dl,0
mov dh,0
mov ah,3
int 13h ;将mini_OS写入软盘之后三个扇区中
ret
boot 程序
将 mini_OS 从软盘的0道0面2扇区及之后的2个扇区中导入内存中的 0:7E00h 中,并将 cs:ip 指向 0:7E00h。
我原本是用的 iret,但是在 dosbox 中可以用 iret 完成功能1,却无法实现引导,修改为使用 retf 后可以成功,同时还要修改栈,具体参考《汇编语言》第十章。
;-------------- boot程序 ----------------
boot: mov ax,0 ;修改栈顶为0:7c00
mov ss,ax
mov sp,7c00h
mov ax,0
mov es,ax
mov bx,7e00h ;es:bx为要读入的内存地址
mov al,3
mov ch,0
mov cl,2
mov dl,0
mov dh,0
mov ah,2
int 13h ;将mini_OS从软盘加载进内存0:7e00h
mov ax,0 ;cs
push ax
mov ax,7e00h ;ip
push ax
retf ;通过retf设置cs:ip
系统重启
最简单的功能,直接将 cs:ip 设置为 FFFF:0000
;----------- (2)重新启动计算机 ------------
rep_system:
mov ax,0ffffh
push ax
mov ax,0
push ax
retf
引导现有操作系统
将 C 盘中的 xp 系统的引导写入 0:7C00h,然后跳转此处执行。
这里也要用 retf 。
;----------- (3)引导现有操作系统 ------------
start_system:
mov ax,0
mov es,ax
mov bx,7c00h ;es:bx指向要写入的位置
mov al,1 ;读取一个扇区
mov ch,0 ;磁道号0
mov cl,1 ;扇区号1
mov dh,0
mov dl,80h ;硬盘c
mov ah,2
int 13h
mov ax,0
push ax
mov ax,7c00h
push ax
retf
完结
综上,本次实验就完成基本完成了,在这个框架上可以完善很多功能,希望大家可以做得更好。
同时感谢我引用的链接的博主们,他们使我少走了不少弯路。
实验代码
本程序要在 cmd 中执行才能写入软盘中,在 dosbox 中无效。
mini_OS 中开头的栈顶设置必须是 0:7c00h,如果设置 0:7e00 会影响引导现有os时的512B内容从而使功能2失效,7c00h + 512b = 7e00h。
全部代码如下:
assume cs:code
stack segment
db 128 dup (0)
stack ends
code segment
start: call loader ;在cmd中运行安装程序,将任务程序写到软盘上
mov ax,4c00h
int 21h
;------------- loader程序 --------------
loader: mov ax,cs
mov es,ax
mov bx,offset boot
mov al,1
mov ch,0
mov cl,1
mov dl,0
mov dh,0
mov ah,3
int 13h ;将boot写入软盘0道0面1扇区
mov bx,offset mini_OS
mov al,3
mov ch,0
mov cl,2
mov dl,0
mov dh,0
mov ah,3
int 13h ;将mini_OS写入软盘之后3个扇区中
ret
;-------------- boot程序 ----------------
boot: mov ax,0
mov ss,ax
mov sp,7c00h
mov ax,0
mov es,ax
mov bx,7e00h ;es:bx为要读入的内存地址
mov al,3
mov ch,0
mov cl,2
mov dl,0
mov dh,0
mov ah,2
int 13h ;将mini_OS从软盘加载进内存0:7e00h
mov ax,0
push ax
mov ax,7e00h
push ax
retf
;-------------------------------------------------
org 7e00h ;这一步定位mini_OS
;---------------- mini_OS操作系统 ----------------
mini_OS:
mov ax,0
mov ss,ax
mov sp,7c00h ;这里必须是7c00
jmp mini_OS_start
strs dw l0,l1,l2,l3,l4,l5,l6,l7,date,date_form
l0 db 'Please choose the following options',0
l1 db '(1) RESET PC',0
l2 db '(2) BOOT OS',0
l3 db '(3) SHOW CLOCK',0
l4 db '(4) SET CLOCK',0
l5 db '---Copyright@kctaig---',0
l6 db 'Enter ESC to return or S to change colors',0
l7 db 'Please enter the date in the following format',0
date db "--/--/-- --:--:--",0
date_form db 9,8,7,4,2,0
table dw str_push,str_pop,str_show
top dw 0
mini_OS_start:
call list_show ;列出功能选项
call check_option ;检测键盘输入
jmp mini_OS_start
ret
;---------------- 输入检查 ----------------
check_option:
call clear_input ;清除键盘缓存
push ax
mov ah,0
int 16h
cmp al,'1'
je fun1
cmp al,'2'
je fun2
cmp al,'3'
je fun3
cmp al,'4'
je fun4
call option_error
call chret
fun1: call rep_system ;调用重启程序
jmp chret
fun2: call start_system
jmp chret
fun3: call show_clock ;显示时钟
jmp chret
fun4: call set_clock
jmp chret
chret: pop ax
ret
;------------ (1)列出功能项 ------------
list_show:
call clear ;清屏
push ax
push bx
push cx
push es
push di
mov ax,0b800h
mov es,ax
mov di,160*10+40
mov bx,0
mov cx,1
call str_output
mov di,160*11+60
mov bx,1
mov cx,4
call str_output
mov di,160*15+50
mov bx,5
mov cx,1
call str_output
pop di
pop es
pop cx
pop bx
pop ax
ret
;----------- (2)重新启动计算机 ------------
rep_system:
mov ax,0ffffh
push ax
mov ax,0
push ax
retf
;----------- (3)引导现有操作系统 ------------
start_system:
mov ax,0
mov es,ax
mov bx,7c00h ;es:bx指向要写入的位置
mov al,1 ;读取一个扇区
mov ch,0 ;磁道号0
mov cl,1 ;扇区号1
mov dh,0
mov dl,80h ;硬盘c
mov ah,2
int 13h
mov ax,0
push ax
mov ax,7c00h
push ax
retf
;-------------- (4)动态显示当前时间 --------------
show_clock:
call clear
call clear_input ;清理键盘缓存区
push ax
push bx
push cx
push di
push ds
push es
mov ax,0b800h
mov es,ax
mov di,160*10+40
mov bx,6
mov cx,1
call str_output ;显示l6字符串
mov ax,1; ;设置颜色
push ax
rep_show:
mov ax,cs
mov ds,ax
mov bx,0
mov ax,0b800h
mov es,ax
mov di,160*12+60 ;es:di输出屏幕行
mov cx,6
s: push cx
mov al,date_form[bx]
push ax
out 70h,al
in al,71h ;此时从端口中获取到数据
mov ah,al
mov cl,4
shr ah,cl
and al,00001111b
add ah,30h
add al,30h ;此时ah为十位,al为个位
mov es:[di],ah
mov es:[di+2],al ;输出
add di,4
pop ax
cmp al,9
jne ok1
mov byte ptr es:[di],'/'
add di,2
jmp ok
ok1: cmp al,8
jne ok2
mov byte ptr es:[di],'/'
add di,2
jmp ok
ok2: cmp al,7
jne ok3
add di,2
jmp ok
ok3: cmp al,4
jne ok4
mov byte ptr es:[di],':'
add di,2
jmp ok
ok4: cmp al,2
jne ok
mov byte ptr es:[di],':'
add di,2 ;输出间隔符号
ok: inc bx
pop cx
loop s
mov ah,1
int 16h ;利用16h中断的1号功能,非阻塞读取
jz rep_call ;如果无缓存字符,继续循环
mov ah,0
int 16h ;将一号功能读取的字符从缓存中清除
cmp al,1bh ;判段是否是esc
je cret
cmp ah,1fh ;用s键来改变颜色
je change_color
jmp rep_call ;若是其他键则继续显示
change_color:
pop ax
inc ax
cmp ax,7 ;设置颜色数量上线
je reset_col
jmp next_col
reset_col: ;重置颜色
mov ax,1
next_col:
push ax ;将下一个颜色入栈
call set_color
rep_call:
jmp rep_show ;循环显示时间
cret: mov ax,7 ;将前景色设置为白色
call set_color
pop ax ;将颜色出栈
pop es
pop ds
pop di
pop cx
pop bx
pop ax
ret
;--------------- (5)修改日期 ----------------
;先将日期写入date字符串中,保证只输入日期和格式正确
;再将字符串中的数字写入cmos中
set_clock:
call clear ;清除屏幕
push ax
push dx
push si
push di
push ds
push es
mov ax,0b800h
mov es,ax
mov di,160*10+40
mov bx,7
mov cx,1
call str_output ;显示l7字符串
mov di,160*12+64
mov bx,8
mov cx,1
call str_output ;显示日期格式
mov dh,12
mov dl,32 ;设置字符串在屏幕上的行列位置
mov ax,cs
mov ds,ax
mov si,offset date ;ds:si指向字符栈空间
call get_str ;输入新日期
call set_cmos ;将新日期写入cmos中
pop es
pop ds
pop di
pop si
pop dx
pop ax
ret
;--------------- 修改cmos中71端口的内容 ---------------
set_cmos:
push ax
push bx
push cx
push si
mov bx,0
mov cx,6
cmos_s: call get_date_cut ;此时al中为日期
mov ah,al
mov al,date_form[bx]
out 70h,al ;将日期对应地址写入70h端口
mov al,ah
out 71h,al ;修改cmos中的内容
inc bx
loop cmos_s
pop cx
pop bx
pop ax
ret
;-------------- 截取date中年、月、日等 ----------------
;参数bx表示截取的位置
;al存储截取出的日期
get_date_cut:
push bx
push cx
push di
push es
mov al,3
mul bl
mov bl,al ;计算偏移量
mov ax,cs
mov es,ax
mov di,offset date
add di,bx ;es:di指向date字符串中日期的位置
mov ax,es:[di]
sub al,30h
sub ah,30h
mov cl,4
shl al,cl ;输入的时候先输入的十位在al中
and al,11110000b;保留al中的前四位
add al,ah ;al中为两位数日期的的BCD码
pop es
pop di ;指向date字符串中日期的位置
pop cx
pop bx
ret
;--------------- 接受字符串的子程序 ---------------
get_str:call clear_input
push ax
get_char:
mov ah,0
int 16h ;从缓存区中取字符
cmp al,20h
jb not_char ;ASCII码小于20h不是字符
mov ah,0
call str_stack ;添加字符
mov ah,2
call str_stack ;显示栈中字符
jmp get_char
not_char:
cmp ah,0eh
je backspace ;退格键的扫描码
cmp ah,1ch
je enter ;回车键的扫描码
jmp get_char
backspace:
mov ah,1
call str_stack ;删除一个字符
mov ah,2
call str_stack ;显示栈中字符
jmp get_char
enter:
mov al,0
mov ah,0
call str_stack ;字符串末尾加0
mov ah,2
call str_stack
pop ax ;字符串输入完成
ret
;--------------- 字符栈的入栈、出栈和显示 -----------------
str_stack:
push bx
push dx
push di
push es
cmp ah,2
ja sret
mov bl,ah
mov bh,0
add bx,bx
jmp word ptr table[bx]
str_push:
mov bx,top
mov [si][bx],al
inc top
jmp sret
str_pop:
cmp top,0
je sret
dec top
mov bx,top
mov al,[si][bx]
jmp sret
str_show:
mov bx,0b800h
mov es,bx
mov al,160
mov ah,0
mul dh
mov di,ax
add dl,dl
mov dh,0
add di,dx ;es:di表示在屏幕中要输入的位置
mov bx,0
char_show:
cmp bx,top
jne char_not_empty
jmp sret
char_not_empty:
mov al,[si][bx]
mov es:[di],al
inc bx
add di,2
jmp char_show
sret: pop es
pop di
pop dx
pop bx
ret
;----------------- 选项输入错误 -------------------
option_error:
call check_option
ret
;----------------- 字符串输出 ---------------------
;es:di表示屏幕显示位置
;bx表示字符串在strs表中的位置
;cx表示要连续输出的字符串个数
str_output:
push si
push di
add bx,bx
s0: mov si,strs[bx] ;将定址表strs中第(bx)个的偏移地址传递到si中
s1: cmp byte ptr cs:[si],0
je next_raw ;若等于零则结束该字符串
mov al,cs:[si]
mov es:[di],al
inc si
add di,2
jmp short s1
next_raw:
add bx,2 ;下一个字符串,注意是加2
pop di
add di,160 ;屏幕下一行
push di
loop s0
pop di ;将屏幕显示的位置pop掉
pop si
ret
;----------- 清屏,将显存中当前屏幕中的字符设为空格符 -----------
clear: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
clear_char:
mov byte ptr es:[bx],' '
add bx,2
loop clear_char
pop es
pop cx
pop bx
ret
;---------------- 设置前景色 ----------------
set_color:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sc: and byte ptr es:[bx],11111000b
or es:[bx],al ;改为al中的颜色
add bx,2
loop sc
pop es
pop cx
pop bx
ret
;--------------- clear_input_buffer --------------
clear_input:
push ax
clear_word:
mov ah,1
int 16h
jz clear_input_ret ;无读取则退出循环
mov ah,0
int 16h
call clear_word ;将非阻塞读取的字符从buffer中删除
clear_input_ret:
pop ax
ret
code ends
end start