汇编语言 第15章 外中断 扩编中断例程的设计思路及注意事项(附书上示例及实验代码+注释)

目录

0. 任务概括

1. 实现思路

2. 流程分析 

3. 代码框架

4. 注意事项

5. 书中示例及实验代码

6. 实验改进

7. 章节总结


0. 任务概括

实现功能:当进入n号中断时,在原中断例程基础上实现额外的自定义功能并正常返回

参考材料:《汇编语言》(第4版)王爽 第15章 外中断


1. 实现思路

  • 编写实现自定义功能的中断例程int n
  • 将向量表n中的入口地址指向该段代码地址
  • 运行程序,期间可以通过引发该中断例程进入我们自己所编写的逻辑,额外实现自定义功能
  • 如果是临时程序,程序结束后恢复向量表中的入口地址

2. 流程分析 

首先我们分析一下原来的int n指令的执行流程:

  1. CPU执行int指令
    1. 取中断类型码n
    2. 标志寄存器入栈
    3. IF=0,TF=0
    4. CS和IP入栈
    5. 跳转向量表n的入口地址
  2. 执行int n中断例程
    1. 处理细节
    2. iret

注:

  • 2.2处 iret 的执行细节
    1. pop IP (对应1.4处的push IP)
    2. pop CS (对应1.4处的push CS)
    3. popf (对应1.2处的pushf)

后面我们将会注意到执行自定义int n时在这个细节上会有什么不同

接着想象一下自定义int n指令需要执行的流程:

  1. CPU执行int指令
    1. 取中断类型码n
    2. 标志寄存器入栈
    3. IF=0,TF=0
    4. CS和IP入栈
    5. 跳转向量表n的入口地址(此时入口地址已被更改为我们自己编写的中断例程的地址)
  2. 执行自定义int n中断例程
    1. 标志寄存器入栈
    2. IF=0,TF=0 (由于在进入中断例程时CPU已经执行了该操作,因此不需要在自己编写的代码中重新实现这一步骤)
    3. CS和IP入栈
    4. 跳转到原来的中断例程代码处(可以通过call 原中断例程地址指令与2.3合并为一步执行)
  3. 执行原来的int n中断例程
    1. 处理其它细节
    2. iret
  4. 回到自定义中断例程
    1. 进行自定义功能处理
    2. iret

注:

  • 3.2处 iret 的执行细节
    1. pop IP (对应2.3处的push IP)
    2. pop CS (对应2.3处的push CS)
    3. popf (对应2.1处的pushf)
  • 4.2处 iret 的执行细节
    1. pop IP (对应1.4处的push IP)
    2. pop CS (对应1.4处的push CS)
    3. popf (对应1.2处的pushf)

通过分析我们能够对int指令背后的执行逻辑有一个清楚的认识


3. 代码框架

通过上面的分析我们可以写出如下的基本框架:

    pushf
    call 原中断例程入口
    处理自定义功能
    iret

p.s. 与调用子程序类似,用到的寄存器的保存与恢复步骤也不要遗漏


4. 注意事项

  • 调用原有中断例程及返回时关注栈内元素,注意iret用法
  • 由于需要调用原来的中断例程,因此需要保存该入口地址以便自定义中断例程程序能够随时获取
  • 由于更改向量表地址期间响应中断可能引起地址混乱,因此我们需要令更改过程中禁止中断发生,即在更改向量表之前将IF置为0,更改完成后恢复IF
    cli ;禁止中断发生

    ;改变向量表n处的地址
    mov 0:[n*4],ip
    mov 0:[n*4+2],cs
    ;改变操作完成

    sti ;允许中断发生
  • 如果是临时扩编中断例程,程序结束后还应恢复向量表中的入口地址

5. 书中示例及实验代码

我们来看几段代码:

  • 第276页 15.4 编写int9中断例程
  • 任务描述:实现功能 依次显示字符a到z,按下Esc后改变颜色,其他的键照常处理
  • 实现方法:扩编int9中断例程
assume cs:code

data segment
    dw 0,0
data ends

stack segment
    db 100H dup (0)
stack ends

code segment
;功能:依次显示字符a到z,按下Esc后改变颜色,其他的键照常处理
;实现方法:扩编int9中断例程
start:
    ;匹配数据段与栈段
    mov ax,data
    mov ds,ax
    mov ax,stack
    mov ss,ax
    mov sp,100H

    ;保存BIOS的int9中断例程的入口地址
    mov ax,0
    mov es,ax
    push es:[9*4]
    pop ds:[0]
    push es:[9*4+2]
    pop ds:[2]

    ;设置新的int9中断例程的入口地址
    cli ;禁止中断发生,防止更改入口地址过程中发生中断导致跳转地址错误
    mov word ptr es:[9*4],offset int9
    mov es:[9*4+2],cs
    sti ;允许中断发生

    ;依次显示字符a到z
    mov ax,0b800H
    mov es,ax
    mov di,160*12+40*2
    ;属性保存在ch
    mov ch,1
    ;字符保存在cl
    mov cl,'a'
show:
    mov es:[di],cx
    ;延迟
    call delay
    inc cl
    cmp cl,'z'
    jna short show

    ;恢复BIOS的int9中断例程的入口地址
    cli ;禁止中断发生,原因同上
    mov ax,0
    mov es,ax
    push ds:[0]
    pop es:[9*4]
    push ds:[2]
    pop es:[9*4+2]
    sti ;允许中断发生

    ;结束程序
    mov ax,4c00H
    int 21H

;延迟方法
delay:
    push ax
    push dx
    mov dx,3H
decrease:
    sub ax,1
    sbb dx,0
    cmp ax,0
    jne decrease
    cmp dx,0
    jne decrease
    pop dx
    pop ax
    ret

;自定义int9中断例程
int9:
    ;保存所用寄存器
    push ax
    push bx

    ;从端口获取键盘扫描码
    in al,60H

    ;标志寄存器入栈
    pushf

    ;调用BIOS的int9中断例程
    call dword ptr ds:[0]

    ;当键盘扫描码为1,即输入为Esc时
    cmp al,1
    jne int9ret
    
    ;下一个字符开始改变颜色
    inc ch

int9ret:
    ;恢复所用寄存器
    pop bx
    pop ax
    ;返回
    iret

code ends
end start
  • 第282页 15.5 安装新的int9中断例程
  • 任务描述:实现功能 按下F1键后改变当前屏幕的显示颜色,其他的键照常处理
  • 实现方法:扩编int9中断例程,并将代码安装到安全空间0:200H中
assume cs:code

stack segment
    db 100H dup(0)
stack ends

code segment
;功能:安装自定义int9中断例程,使得按下F1键后改变当前屏幕的显示颜色,其他的键照常处理
;实现方法:扩编int9中断例程,并将代码安装到安全空间0:200H中
start:
    ;匹配栈段
    mov ax,stack
    mov ss,ax
    mov sp,100H

    ;在安全空间0:200H处保存原int9中断例程的入口地址
    mov ax,0
    mov es,ax
    push es:[9*4]
    pop es:[200H]
    push es:[9*4+2]
    pop es:[202H]

    ;将自定义int9代码复制到安全的空间0:204H中
    mov ax,cs
    mov ds,ax
    mov si,offset int9
    mov di,204H ;这里es已经为0
    mov cx,offset int9end-offset int9
    cld
    rep movsb

    ;安全修改向量表入口地址
    cli
    mov word ptr es:[9*4],204H
    mov word ptr es:[9*4+2],0
    sti

    ;结束安装程序
    mov ax,4c00H
    int 21H

;自定义int9中断例程
int9:
    ;所用寄存器压栈
    push ax
    push bx
    push cx
    push es

    ;获取键盘扫描码
    in al,60H

    ;调用原int9中断例程
    pushf
    call dword ptr cs:[200H]

    ;当键盘输入F1
    cmp al,1
    jne int9ret
    
    ;全屏改变颜色
    mov ax,0b800H
    mov es,ax
    mov bx,1
    mov cx,2000
change:
    inc byte ptr es:[bx]
    add bx,2
    loop change

int9ret:
    ;恢复寄存器,返回
    pop es
    pop cx
    pop bx
    pop ax
    iret
int9end:
    nop

code ends
end start
  • 第285页 实验15 安装新的int9中断例程
  • 任务描述:实现功能 松开A键后改变出现满屏'A',其他的键照常处理
  • 实现方法:扩编int9中断例程,并将代码安装到安全空间0:200H中
assume cs:code

stack segment
    db 100H dup(0)
stack ends

code segment
;功能:松开A键后改变出现满屏'A',其他的键照常处理
;实现方法:扩编int9中断例程,并将代码安装到安全空间0:200H中
start:
    ;匹配栈段
    mov ax,stack
    mov ss,ax
    mov sp,100H

    ;在安全空间0:200H处保存原int9中断例程的入口地址
    mov ax,0
    mov es,ax
    push es:[9*4]
    pop es:[200H]
    push es:[9*4+2]
    pop es:[202H]

    ;安全修改向量表int 9入口地址
    cli
    mov word ptr es:[9*4],204H
    mov word ptr es:[9*4+2],0
    sti

    ;安装代码到全空间0:204H处
    push cs
    pop ds
    mov si,offset int9
    mov di,204H
    mov cx,offset int9end-offset int9
    cld
    rep movsb

    ;结束安装程序
    mov ax,4c00H
    int 21H

;自定义int 9中断例程
int9:
    ;保存寄存器
    push ax
    push bx
    push cx
    push es

    ;获取键盘扫描码
    in al,60H
    
    ;调用原例程
    pushf
    call dword ptr cs:[200H]

    ;当松开键A
    cmp al,9eH ;1eH+80H
    jne int9ret

    ;全屏为修改字符'A'
    mov ax,0b800H
    mov es,ax
    mov bx,0
    mov cx,2000
show_a:
    mov byte ptr es:[bx],'A'
    add bx,2
    loop show_a

int9ret:
    ;恢复寄存器并返回
    pop es
    pop cx
    pop bx
    pop ax
    iret
int9end:
    nop
code ends
end start

6. 实验改进

经过测试我们发现,当我们重复安装自定义中断例程时会发生错误,这是因为第二次安装时从向量表中获取的入口地址指向的是第一次安装后的入口地址,即0:204H,而我们希望保存的其实是系统提供的中断例程的入口地址。

那么真正的原入口地址在哪里呢?

  • 我们知道,第一次安装时我们已经将该入口地址存在代码开头,即0:200H处,因此我们可以从0:200H处获取到真正需要保存的入口地址。

我们如何判断当前是否为首次安装呢?

  • 很简单,在书上的课程中我们将所有的扩展中断例程都安装在了0:200H处,因此我们只需要检测内存单元[0:200H]是否为0就可以基本判断我们先前是否安装过自定义中断例程(当原入口地址的IP为0时这种判断方法是错误的,当然有其它更好的检测方法)。

如果不是首次安装,我们应该做什么?

  • 由于课程中我们都是将程序安装在0:204H处,因此向量表中保存的新地址已经指向了该代码段,所以对我们来说最简单的方法其实是跳过修改向量表和保存原入口地址的过程,直接从0:204H处开始安装代码
    mov ax,0
    mov es,ax

    mov ax,es:[200H]
    cmp ax,0
    jne short no_op ;no operation

我们来看一下修改后的实验代码:

另外一个课程练习也可以写上类似的安全代码

assume cs:code

stack segment
    db 100H dup(0)
stack ends

code segment
;功能:松开A键后改变出现满屏'A',其他的键照常处理
;实现方法:扩编int9中断例程,并将代码安装到安全空间0:200H中
start:
    ;匹配栈段
    mov ax,stack
    mov ss,ax
    mov sp,100H

    ;在安全空间0:200H处保存原int9中断例程的入口地址
    mov ax,0
    mov es,ax

    ;通过检查[0:200H]是否为0来确定是否为首次安装
    mov ax,es:[200H]
    cmp ax,0
    jne short no_op ;如果不是首次安装则只需进行代码复制

    ;如果是首次安装则进行下列操作
    push es:[9*4]
    pop es:[200H]
    push es:[9*4+2]
    pop es:[202H]

    ;安全修改向量表int 9入口地址
    cli
    mov word ptr es:[9*4],204H
    mov word ptr es:[9*4+2],0
    sti

no_op:
    ;安装代码到全空间0:204H处
    push cs
    pop ds
    mov si,offset int9
    mov di,204H
    mov cx,offset int9end-offset int9
    cld
    rep movsb

    ;结束安装程序
    mov ax,4c00H
    int 21H

;自定义int 9中断例程
int9:
    ;保存寄存器
    push ax
    push bx
    push cx
    push es

    ;获取键盘扫描码
    in al,60H
    
    ;调用原例程
    pushf
    call dword ptr cs:[200H]

    ;当松开键A
    cmp al,9eH ;1eH+80H
    jne int9ret

    ;全屏为修改字符'A'
    mov ax,0b800H
    mov es,ax
    mov bx,0
    mov cx,2000
show_a:
    mov byte ptr es:[bx],'A'
    add bx,2
    loop show_a

int9ret:
    ;恢复寄存器并返回
    pop es
    pop cx
    pop bx
    pop ax
    iret
int9end:
    nop
code ends
end start

 除了这种方法,另一种解决办法是写一个卸载程序,需要在每次安装自定义程序前先删除已安装程序:

  • 检查0:200H处是否已存在安装程序
  • 如果存在,依据课程中编写代码的逻辑,则说明此时向量表某处的入口地址并非指向系统自带的地址,应当复原该处入口地址(这里存在一个问题,在课程中我们所编写的安装程序中并没有保存过所修改的中断例程是第几号中断例程n,因此如果我们想要使用这种方法,需要在安装程序时将中断编号n一同存入代码安装处附近,这样卸载程序才能够获取到向量表中应当复原的中断编号),接着将程序安装处附近的原入口地址复原到该编号处
  • 最后将0:200H处附近几个单元清零,表示允许下次直接安装程序

如果一定要在现有代码基础上编写卸载程序的话,其实还可以遍历中断向量表,找到相邻分别存储204H0H的单元,也可以确定修改过的中断编号。

由于需修改内容较多,且实现逻辑并不复杂,因此这里仅仅提供一个思路,就不具体编写出来啦~


7. 章节总结

这一章简单讲解了外中断的工作原理及我们如何获取和利用中断信息实现一些自定义功能

通过这一章的练习,我们应该做到

  • 理解CPU响应外部中断的大致工作原理及部分细节
  • 能够简单利用中断例程在保留原有功能的基础上实现自定义功能

祝大家在计算机的学习之路上前进得坚定而顺利,不留遗憾,每天都有收获~~~

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
普中单片机开发板是一种用于学习和开发单片机的硬件平台,而Proteus则是一款常用的电子电路仿真软件。结合这两个工具,可以进行单片机的仿真实验。 在Proteus中进行单片机仿真,首先需要在软件中选取合适的单片机型号,然后通过连接电路图和代码实现对单片机的仿真。仿真可以帮助我们在实际硬件未准备齐全或不易操作的情况下进行实验验证。 在使用Proteus进行单片机仿真时,需要在代码中添加适当的注释注释是对代码功能的解释和说明,有助于提高代码的可读性和可维护性。 注释可以包括以下信息: 1. 代码作者:在代码的开头可以注明编写代码的人员的名字或者团队的名称。 2. 代码的功能:对代码所实现的功能进行简要的说明,让其他人可以快速了解代码的作用。 3. 输入输出说明:对代码中使用的各个输入和输出参数进行说明,包括其类型、取值范围等信息。 4. 算法原理:对于涉及到较为复杂的算法,可以通过注释进行简单的说明,帮助其他人理解代码的实现过程。 5. 重要变量说明:对于代码中的重要变量,可以进行注释说明其作用和意义,以及在代码中的使用方法。 在为Proteus仿真实验程添加注释时,我们应该清晰明了地解释每个实验的目的和步骤,并且注释的语言应该简洁明了,易于理解。此,还要注意注释的格式清晰,尽量遵循代码注释的规范,方便其他人阅读和修改代码。 总之,通过在普中单片机开发板Proteus仿真实验程中添加注释,可以提高代码的可读性和可维护性,方便其他人理解和使用代码,也有助于培养良好的编程习惯和规范。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值