一道题
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