实验目的:
1、了解监控程序执行用户程序的主要工作
2、了解一种用户程序的格式与运行要求
3、加深对监控程序概念的理解
4、掌握加载用户程序方法
5、掌握几个BIOS调用和简单的磁盘空间管理
实验要求:
1、知道引导扇区程序实现用户程序加载的意义
2、掌握COM/BIN等一种可执行的用户程序格式与运行要求
3、将自己实验一的引导扇区程序修改为3-4个不同版本的COM格式程序,每个程序缩小显示区域,在屏幕特定区域显示,用以测试监控程序,在1.44MB软驱映像中存储这些程序。
4、重写1.44MB软驱引导程序,利用BIOS调用,实现一个能执行COM格式用户程序的监控程序。
5、设计一种简单命令,实现用命令交互执行在1.44MB软驱映像中存储几个用户程序。
6、编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
实验内容:
(1) 将自己实验一的引导扇区程序修改为一个的COM格式程序,程序缩小显示区域,在屏幕第一个1/4区域显示,显示一些信息后,程序会结束退出,可以在DOS中运行。在1.44MB软驱映像中制定一个或多个扇区,存储这个用户程序a。
相似地、将自己实验一的引导扇区程序修改为第二、第三、第四个的COM格式程序,程序缩小显示区域,在屏幕第二、第三、第四个1/4区域显示,在1.44MB软驱映像中制定一个或多个扇区,存储用户程序b、用户程序c、用户程序d。
(2) 重写1.44MB软驱引导程序,利用BIOS调用,实现一个能执行COM格式用户程序的监控程序。程序可以按操作选择,执行一个或几个用户程序。解决加载用户程序和返回监控程序的问题,执行完一个用户程序后,可以执行下一个。
(3)设计一种命令,可以在一个命令中指定某种顺序执行若干个用户程序。可以反复接受命令。
(4)在映像盘上,设计一个表格,记录盘上有几个用户程序,放在那个位置等等信息,如果可以,让监控程序显示出表格信息。
(5)拓展自己的软件项目管理目录,管理实验项目相关文档
获取用户输入和读扇区的bios中断调用
上面的任务要求中包含了根据用户的操作选择要执行的用户COM程序。为此我们要在引导扇区以外的扇区存储我们的abcd四个子程序,在需要执行时,boot应该能把对应的子程序放入内存,并用跳转指令执行。
BIOS的中断int 16h满足我们的获取用户输入要求,ah=0时,有
从键盘读入字符送AL寄存器。执行时,等待键盘输入,一旦输入,字符的ASCII码放入AL中。若AL=0,则AH为输入的扩展码。
BIOS的中断int 13h满足我们的读扇区要求,设ah=02h,则有
al = 要读扇区数,ch=柱面(磁道)号,cl=起始扇区号,dh=磁头号,dl=驱动器号(0表示A盘),ea:bx->数据缓冲区。
我们需要自己定义ch、cl和dh,这个在1.44MB的软盘上是有给定的公式的
这里因为我们的代码量并不多,我们会把它装在前几个扇区方便读取。我们先尝试一下使用上面两个中断读取特殊的扇区内容,并把它们打印出来。
input_read_test.asm
OffSetOfUserStr equ 0A100h
org 7c00h
%macro print 4 ; string, length, x, y
mov ax, cs
mov ds, ax
mov bp, %1
mov ax, ds
mov es, ax
mov cx, %2
mov ah, 13h
mov al, 00h
mov bh, 00h
mov bl, 07h ; 黑底白字
mov dh, %3
mov dl, %4
int 10h
%endmacro
datadef:
msg db 'This is a test program. Enter 1~4 to get 10 ascii from target sector.'
msglen equ ($-msg)
msg1 db 'Press any key for continue.'
msglen1 equ ($-msg1)
sectorNum db '1'
Entrance:
call cls
print msg, msglen, 0, 0
mov ah, 0
int 16h ;调用16h中断,从键盘获取ascii码输入并存在al中
cmp al, '1'
jl Entrance
cmp al, '4' ;避免无效键盘输入
jg Entrance
mov [sectorNum], al ;获得键盘输入的扇区号
mov cl, [sectorNum]
sub cl, '0'-1 ;读扇区号
mov ax, cs ;定位es
mov es, ax
mov ah, 2 ;读扇区
mov al, 1 ;读扇区数
mov dl, 0 ;驱动器号
mov dh, 0 ;磁头号
mov ch, 0 ;柱面号
mov bx, OffSetOfUserStr ;数据缓冲区
int 13H
print OffSetOfUserStr, 10, 1, 0
print msg1, msglen1, 2, 0
mov ah, 0
int 16h
jmp Entrance
cls:
mov ax, 0B800h
mov es, ax
mov si, 0
mov cx, 80*25 ; 循环次数
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
ret
mov ax, cs
mov ds, ax
mov es, ax
times 510-($-$$) db 0
dw 0xaa55
借助中断调用和简单循环,程序可以读取目标扇区的内容并打印出来。这里我把首扇区后的四个扇区用a、b、c和d填满,在程序的执行中会询问用户要访问的扇区号(1-4),然后程序把一个扇区的512字节都拉到内存的0A100位置,然后打印前10个字符。
执行程序与监控行为
执行程序事实上比较简单,只需要在上面的代码做一些改动,让程序jmp到我们的内存目标位置运行即可。但是如何用主程序监控子程序的运行是需要技巧的,毕竟我们的编程仍然是串行编程,而我们又不方便在子程序的代码上直接做更改来实现监控功能(这是编译器才会做的事情)。这时我们需要用到的技巧是自定义中断。
bios的中断执行会从以0000h为起点的一张中断向量表中查找中断,每个中断向量占四字节。2字节用做偏移,2字节用做基址。而bios有很多空的向量可以留给我们自己定义,我们可以在特定的位置装入特殊的地址作为中断服务程序的偏移地址。
我们把上面的程序读扇区程序做一些改动,在读入扇区并打印10个ascii字符后就陷入无限循环的沉睡,沉睡过程中我们会不断调用一种我们自己设计的中断,检测是否有CTRL+Z的组合键,如果有才跳出循环,回到主界面再次请示用户要不要读扇区。
键盘输入在无即时性的输入请求时,会先放入缓冲区。我们的中断服务程序做的事情就是扫描键盘缓冲区,判断是不是CTRL+Z的组合键,如果是就退出死循环并回到入口。
至于CTRL+Z的扫描码是多少我们并不知道,但是我们可以写个程序看一下。我们运行一下int 16h再输入CTRL+Z,查看ax寄存器的值为2c1ah,则我们在做判断时只需要cmp ax和2c1ah即可。
self_defined_int.asm(片段)
mov ax, 0000h ; 中断向量表从0h开始
mov es, ax
mov ax, 20h ; 重定义20号中断
mov bx, 4 ; 每个中断向量是4字节的地址,所以20h乘以4
mul bx
mov si, ax ; 偏移
mov ax, int20h
mov [es:si], ax ; 把我们的int20的偏移地址作为20号中断的中断服务程序
add si, 2
mov ax, cs ; 放入代码段, 符合中断向量的格式(前两字节为偏移,后两字节为代码段,且为大端序)
mov [es:si], ax
int20h:
mov ah, 01h ;缓冲区检测
int 16h
jz noclick ;缓冲区无按键
mov ah, 00h
int 16h
cmp ax, 2c1ah ; 检测Ctrl + Z
jne noclick
jmp Entrance ; 如果是则退出程序
noclick:
iret
可以看见,如果不按这个组合键,程序就只能被卡在这里。而使用组合键后就能跳出循环回到主界面,这个功能就是我们的监控程序监控程序运行的方式。我们在子程序中的循环中放置int 20h的中断请求,这个中断请求在循环中反复查看键盘缓冲区,直到缓冲区中出现CTRL+Z的组合键,程序才会终止运行。
COM程序执行时的基址和偏移
我们在用上面的13h调用装入用户程序之后,会把程序装到我们指定的内存位置OffSetOfUserPrg=0100h。那么我们在子程序执行时要怎么访问内存中的数据呢?
我们要搞明白的是ORG是什么,它是一个汇编伪指令,使用的目的是告诉编译器这段代码被放在哪里了。有了这个org,我们在这个子程序中直接访问内存时,会加上一个固有的偏移。也就是说,org只有在我们需要认为读写内存时才会生效。以实验一为例,bios识别到引导扇区以后,会强行把这段代码放入内存的7c00h以下的512字节。我们的引导扇区代码中会有一些数据声明,比如name db ‘huangwx’。name和代码掺在一起,是一个偏移地址。org 7c00h给出的是基址地址,这就让程序执行时,使用的是基址地址+偏移地址来找数据。比如mov ax,[02h],就是把7c00h+02h的16字节的数据放入ax寄存器。
搞懂org是什么以后,上面的问题就变得不是问题了。我们的监控程序把目标扇区的COM程序装入内存的0100h位置,如果不在编写COM子程序时使用org 0100h,子程序的寻址就会出错。
监控程序实践
我们按照上面所述,编写4个COM格式的用户程序,并把它们塞入第2、3、4、5扇区等待监控程序读取。这4个用户程序在运行时定期执行int 20h号中断来检测用户有没有按下ctrl+z来中断程序。
bootloader.asm
OffSetOfUserPrg equ 00100h
org 7c00h
%macro print 4 ; string, length, x, y
mov ax, cs
mov ds, ax
mov bp, %1
mov ax, ds
mov es, ax
mov cx, %2
mov ah, 13h
mov al, 00h
mov bh, 00h
mov bl, 07h ; 黑底白字
mov dh, %3
mov dl, %4
int 10h
%endmacro
; 这里我们做一个字符串打印的封装简化代码
mov ax, 0000h ; 中断向量表从0h开始
mov es, ax
mov ax, 20h ; 重定义20号中断
mov bx, 4 ; 每个中断向量是4字节的地址,所以20h乘以4
mul bx
mov si, ax ; 偏移
mov ax, int20h
mov [es:si], ax ; 把我们的int20的偏移地址作为20号中断的中断服务程序
add si, 2
mov ax, cs ; 放入代码段, 符合中断向量的格式(前两字节为偏移,后两字节为代码段,且为大端序)
mov [es:si], ax
begin:
call cls
print msg, msglen, 0, 0
input:
mov ah, 0
int 16h ;调用16h中断,从键盘获取ascii码输入并存在al中
cmp al, '1'
jl input
cmp al, '4' ;避免无效键盘输入
jg input
mov [sectorNum], al ;获得键盘输入的扇区号
call cls
print msg1, msglen1, 0, 0
print sectorNum, 1, 0, 16
mov cl, [sectorNum]
sub cl, '0'-1 ;从第二个扇区开始
mov ax, cs ;定位es
mov es, ax
mov ah, 2 ;读扇区
mov al, 1 ;读扇区号
mov dl, 0 ;驱动器号
mov dh, 0 ;磁头号
mov ch, 0 ;柱面号
mov bx, OffSetOfUserPrg ;数据缓冲区
int 13H
jmp OffSetOfUserPrg ;跳转去执行目标程序
cls:
mov ax, 0B800h
mov es, ax
mov si, 0
mov cx, 80*25 ; 循环次数
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
ret
; 这段代码把B800h(显存)下的一段内存清空,起清屏作用
; 自定义的中断号
int20h:
mov ah, 01h ;缓冲区检测
int 16h
jz noclick ;缓冲区无按键
mov ah, 00h
int 16h
cmp ax, 2c1ah ; 检测Ctrl + Z
jne noclick
jmp begin ; 如果是则退出程序
noclick:
iret
datadef:
msg db 'Welcome to BootLoader, press 1~4 to run a program.'
msglen equ ($-msg)
msg1 db 'This is program 0, press Ctrl + Z to return.'
msglen1 equ ($-msg1)
sectorNum db '1'
times 510-($-$$) db 0
dw 0xaa55
用户程序a.asm, 在左上角的1/4窗口实行弹球逻辑
org 00100h
mov ax, cs
mov ds, ax
mov es, ax
mov al, byte[upper]
add al, 5
mov byte[x],al
mov al, byte[left]
add al, 5
mov byte[y],al
loop1:
dec word[count] ; 递减计数变量
jnz loop1 ; >0:跳转;
mov word[count],delay
dec word[dcount] ; 递减计数变量
jnz loop1
mov word[count],delay
mov word[dcount],ddelay
; 以上是用一个二重循环实现时延50000*580个单位时间
jmp Entrance ;进行一个周期的工作
jmp $ ;halt
Entrance:
int 20h
jmp BoundaryCheckx
DispStr:
call Clear
call Reset
mov ax, Message ;打印字符串
mov bp, ax
mov cl, byte[Strlen] ;字符串长
mov ch, 0
mov ax, 01301h ;写模式
mov bx, 000fh ;页号0,黑底白字
mov dh, byte[x] ;行=x
mov dl, byte[y] ;列=y
int 10h ;10h号接口
Updatexy:
mov al, byte[x]
add al, byte[vx]
mov byte[x], al
mov al, byte[y]
add al, byte[vy]
mov byte[y], al
jmp loop1 ;无限循环
BoundaryCheckx:
mov al, byte[x]
add al, byte[vx] ;预测下一刻的x
cmp al, byte[upper] ;如果x小于上边界
jl Changevx ;更新vx
cmp al, byte[lower] ;如果x大于下边界
jg Changevx ;更新vx
BoundaryChecky:
mov al, byte[y]
add al, byte[vy]
cmp al, byte[left] ;如果y小于左边界
jl Changevy ;更新vy
add al, byte[Strlen];预测下一刻的yr=y+字符串长
cmp al, byte[right] ;如果yr大于下边界
jg Changevy ;更新vy
jmp DispStr ;如果不需要更新vx vy就继续打印流程
Changevx:
neg byte[vx]
jmp BoundaryChecky
Changevy:
neg byte[vy]
jmp DispStr
Clear:
mov ax, 0B800h
mov es, ax
mov si, 160
mov cx, 80*24 ; 循环次数
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
ret
;通过直接修改显存实现的清屏函数
Reset:
mov ax, cs
mov ds, ax
mov ax, ds
mov es, ax
ret
;把cs ds和es指向相同的内存
Message: db "17310031"
Strlen db $-Message
delay equ 50000
ddelay equ 2000
count dw delay
dcount dw ddelay
clearcount db 0
vx db 1
vy db 1
left db 0
upper db 1
right db 39
lower db 12
x db 0
y db 0
times 512-($-$$) db 0
运行结果
用户程序运行的任意时刻按下组合键都可以中断程序运行并jmp回主监控程序. 到此, 最基本的监控程序就实现完毕了.
拓展性工作.
- 这里我们的程序只有不到512字节, 当超过512字节时我们要做的可能是要读取多个扇区, 比如以链式访问的形式访问多个不同磁盘位置的扇区, 并把它们装入内存中连续的一段地址.
- 又或者, 我们可以在监控程序中设计按照一定顺序执行几个用户程序的自动化监控程序. 比如定义新的中断, 不再是接收用户键盘组合键才停止运行, 而是用一个计数器标识识别什么时候该触发中断, 返回监控程序.
- 再比如, 我们的用户程序可以放在软盘的任意扇区, 这个扇区不是我们人为指定的而是机器指定的, 机器会把程序序号—扇区号的一一对应记录在一张软盘里的表中. 监控程序运行前我们先把这张表加载到内存中, 然后用户就不需要输入扇区号才能装填用户程序, 只需要输入用户程序的序号, 监控程序就自动在内存中查询到对应的扇区是哪一个