一道题

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的段地址和偏移地址,分别存入dsdx

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 取出保存的旧中断向量

这里说到,需要将中断向量的段地址和偏移地址分别存入DSDX寄存器中。而在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