一道题

作者: 焜_8899 | 来源:发表于2020-06-20 18:00 被阅读0次

    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
    

    相关文章

      网友评论

          本文标题:一道题

          本文链接:https://www.haomeiwen.com/subject/dfrbxktx.html