1. 题目
背景
中断是一个很有效率的IO通讯方式。在PC里,1ch是一个计时器计时中断,每秒钟会中断18.2次。在原本的ISR(interrupt service routine,中断服务程序)中,1ch只有一条“iret”指令。实际上,原本的1ch ISR什么都没做。你需要重写一个新的1ch ISR以完成下述任务。
任务
每3秒在屏幕上打印“bell ring”并响铃。原本的ISR必须在程序结束前被存回原本的位置。
提示
在主程序中,通过调用DOS系统功能将一个新的ISR向量安装在中断向量表中。通过执行INT 21h的25h号功能可以安装中断向量。须注意应当先用DOS INT 21h的35h号功能将原本的中断向量保存下来。响铃ISR被定义为一个用于打印信息与响铃的过程。
2. 我的答案
虽然程序中的注释用了比较蹩脚的英文,但下面有分步解析,可以把此页面用两个标签页同时打开对比着看。
; This is the answer for the question.
; @author Hao Yukun
; @version 2020/6/19
stack segment
db 128 dup(0)
stack ends
data segment
count dw 1
message db 'bell ring', 0dh, 0ah, '$'
data ends
code segment
assume cs:code, ds:data, ss:stack
start:
mov ax, data
mov ds, ax
; get the old interrupt vector
mov ah, 35h
mov al, 1ch
int 21h
push es
push bx
; install the new interrupt vector
push ds ; save the data before using the register
mov ax, seg ring
mov ds, ax
mov dx, offset ring
mov ah, 25h
mov al, 1ch
int 21h
pop ds ; return the data into the register
; delay some time for printing and ringing
mov ah, 86h
mov al, 0
mov cx, 98h ; 10s = 10,000,000us
mov dx, 9680h ; 10,000,000 = 0x 98 9680
int 15h
; restore the old interrupt vector
pop dx
pop ds
mov ah, 25h
mov al, 1ch
int 21h
; return to DOS (quit the program)
mov ah, 4ch
int 21h
; ring and print procedure
ring proc near
; ISR
push ds ; save to protect data in stack
push ax
push cx
push dx
dec count ; count the time when 1ch is called
jnz quit_the_interrupt
; print the message "bell ring"
lea dx, message
mov ah, 9
int 21h
; ring
mov dx, 100
in al, 61h
and al, 0fch
sound:
xor al, 02
out 61h, al
mov cx, 1989
delay_for_freg: loop delay_for_freg
dec dx
jnz sound
mov count, 55 ; 55 times of calling 1ch causes approximately 3s
quit_the_interrupt:
pop dx ; restore data from stack
pop cx
pop ax
pop ds
iret ; interrupt return
ring endp
code ends
end start
3. 分步解析
3.1 获取并保存原中断向量
对应代码中注释; get the old interrupt vector
后面的部分。
3.1.1 获取原中断向量
因为题目要求的是重写1ch
中断,所以先将其原本内容取出。于是为al
寄存器赋值为1ch
。
关于取中断向量的方法请看这里。
mov ah, 35h
mov al, 1ch
int 21h
获取到的中断向量的段地址会被存入es
寄存器,偏移地址会被存入bx
寄存器。
3.1.2 保存原中断向量
本程序使用压栈的方式保存原中断向量。
刚才说到,“获取到的中断向量的段地址会被存入es
寄存器,偏移地址会被存入bx
寄存器”,所以将此二者压栈。
push es
push bx
当然,保存原中断向量不止这一种方法,例如
mov word ptr old, bx
mov word ptr old+2, es
对比之下,个人认为压栈的方式比较简单简洁。
3.2 安装新中断向量
对应代码中注释; install the new interrupt vector
后面的部分。
关于安装中断向量的方法请看这里。
push ds ; save the data before using the register
mov ax, seg ring
mov ds, ax
mov dx, offset ring
mov ah, 25h
mov al, 1ch
int 21h
pop ds ; return the data into the register
因为ds
中存了此程序的数据段内容,而在安装新中断时又需要用到ds
寄存器,所以先将其压栈保存,后面再出栈存回。
因为题目要求的是重写1ch
中断,所以为al
寄存器赋值为1ch
。
因为要安装的新中断向量以过程的形式写在了此程序靠后的部分,名称为ring
, 所以取ring
的段地址和偏移地址,分别存入ds
和dx
。
3.3 延时
对应代码中注释; delay some time for printing and ringing
后面的部分。
因为题目要求“每3秒在屏幕上打印‘bell ring’并响铃”,而只有程序运行时间大于3秒、打印并响铃大于1次,才能体现出是每3秒做了一次规定操作,所以需要设置一个大于3秒的延时。
本程序中设置了一个约10秒的延时,关于延时的用法请看这里。
mov ah, 86h
mov al, 0
mov cx, 98h ; 10s = 10,000,000us
mov dx, 9680h ; 10,000,000 = 0x 98 9680
int 15h
其实延时不只有这一种方法,这里介绍了三种方法。
此程序之所以选用这一种方法,是因为可以确定延时的具体时间。
3.4 将旧中断向量装回中断向量表
对应代码中注释; restore the old interrupt vector
后面的部分。
在3.2和这里都讲了如何安装一个中断向量,将旧中断向量装回中断向量表的操作与之同理。
3.4.1 取出保存的旧中断向量
这里说到,需要将中断向量的段地址和偏移地址分别存入DS
和DX
寄存器中。而在3.1.2中,旧中断向量的段地址和偏移地址已经被压栈。于是可直接将栈中内容取出,存入对应寄存器。
pop dx
pop ds
注意:由于栈是先进后出的,所以在进栈和出栈是都需要注意顺序
在3.1.2中,先进栈的是段地址,后进栈的是偏移地址,所以此处先将偏移地址出栈到dx
中,在将段地址出栈到ds
中。
若程序结束运行后,无法向DOS输入任何内容,则有可能是进栈或出栈的顺序出了问题。建议检查一下。
3.4.2 安装旧中断向量
旧中断向量的安装与3.2安装新中断向量的操作相同。
mov ah, 25h
mov al, 1ch
int 21h
3.5 退出程序,返回DOS
对应代码中注释; return to DOS (quit the program)
后面的部分。
查阅资料时发现,结束程序返回DOS的方法不唯一。个人认为这种方法比较简单简洁,并用于此程序中。
mov ah, 4ch
int 21h
3.6 新中断向量过程
根据题目要求,将新中断向量写为了一个过程,对应代码中注释; ring and print procedure
后面的部分。
3.6.1 保护现场
在调用中断的时候,需要对现场进行保护,所以将以下可能受影响的寄存器压栈。
push ds ; save to protect data in stack
push ax
push cx
push dx
3.6.2 计时
由于本程序所使用的1ch
中断会每秒被自动调用18.2次,而题目要求每3秒响一次铃并显示信息,所以需要通过计数以计时。
1ch
每秒中断18.2次,所以每3秒中断54.6次。相当于每中断55次,可以认为大约过了3秒。
因此,在响一次铃并显示信息后,将count
赋值为55
,并在1ch
每次中断的时候减一。这样,每当count
变为零的时候,可以认为距上次响铃与显示信息过了大约3秒,需要再次响铃并显示信息。而在count
非零的时候,需要跳过响铃和显示信息的步骤。
dec count ; count the time when 1ch is called
jnz quit_the_interrupt
; 显示信息与响铃
mov count, 55 ; 55 times of calling 1ch causes approximately 3s
quit_the_interrupt:
; 恢复现场与中断返回
3.6.3 显示信息
对应代码中注释; print the message "bell ring"
后面的部分。
在data segment
部分,要显示的字符串信息已被存入message
中,直接输出即可。
lea dx, message
mov ah, 9
int 21h
3.6.4 响铃
对应代码中注释; ring
后面的部分。
其实这一部分我是存有疑问的。
在学习的过程中,包括大部分网上的资料,都表明扬声器应当如下述步骤使用。
; 1. 开启扬声器
in al,61h
or al,3
out 61h,al
; 2. 初始化8253定时器
mov al,0b6h
out 43h,al
; 3. 播放声音
mov bx,freg
mov al,bl
out 42h,al
mov al,bh
out 42h,al
; 4. 关闭扬声器
in al,61h
and al,0fch
out 61h,al
然而,经试验,中断时使用上述方法只能听到电流声,并没有清楚的响铃声。
在查找资料后,以下方法确认可行,但原因尚不清楚。
mov dx, 100
in al, 61h
and al, 0fch
sound:
xor al, 02
out 61h, al
mov cx, 1989
delay_for_freg: loop delay_for_freg
dec dx
jnz sound
同时,注意到其中的这一段代码:
mov cx, 1989
delay_for_freg: loop delay_for_freg
在介绍延时的方法的时候,这段程序中1989
应表示循环次数。然而,经试验,这里却成了调节音调的高低,原因尚不清楚。
3.6.5 恢复现场
在3.6.1中,通过压栈保护了现场。于是在中断返回前,需要出栈以恢复现场。
pop dx ; restore data from stack
pop cx
pop ax
pop ds
网友评论