目录
0. 任务概括
实现功能:当进入n号中断时,在原中断例程基础上实现额外的自定义功能并正常返回
参考材料:《汇编语言》(第4版)王爽 第15章 外中断
1. 实现思路
- 编写实现自定义功能的中断例程int n
- 将向量表n中的入口地址指向该段代码地址
- 运行程序,期间可以通过引发该中断例程进入我们自己所编写的逻辑,额外实现自定义功能
- 如果是临时程序,程序结束后恢复向量表中的入口地址
2. 流程分析
首先我们分析一下原来的int n指令的执行流程:
- CPU执行int指令
- 取中断类型码n
- 标志寄存器入栈
- IF=0,TF=0
- CS和IP入栈
- 跳转向量表n的入口地址
- 执行int n中断例程
- 处理细节
- iret
注:
- 2.2处 iret 的执行细节
- pop IP (对应1.4处的push IP)
- pop CS (对应1.4处的push CS)
- popf (对应1.2处的pushf)
后面我们将会注意到执行自定义int n时在这个细节上会有什么不同
接着想象一下自定义int n指令需要执行的流程:
- CPU执行int指令
- 取中断类型码n
- 标志寄存器入栈
- IF=0,TF=0
- CS和IP入栈
- 跳转向量表n的入口地址(此时入口地址已被更改为我们自己编写的中断例程的地址)
- 执行自定义int n中断例程
- 标志寄存器入栈
IF=0,TF=0(由于在进入中断例程时CPU已经执行了该操作,因此不需要在自己编写的代码中重新实现这一步骤)- CS和IP入栈
- 跳转到原来的中断例程代码处(可以通过call 原中断例程地址指令与2.3合并为一步执行)
- 执行原来的int n中断例程
- 处理其它细节
- iret
- 回到自定义中断例程
- 进行自定义功能处理
- iret
注:
- 3.2处 iret 的执行细节
- pop IP (对应2.3处的push IP)
- pop CS (对应2.3处的push CS)
- popf (对应2.1处的pushf)
- 4.2处 iret 的执行细节
- pop IP (对应1.4处的push IP)
- pop CS (对应1.4处的push CS)
- 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处附近几个单元清零,表示允许下次直接安装程序
如果一定要在现有代码基础上编写卸载程序的话,其实还可以遍历中断向量表,找到相邻分别存储204H与0H的单元,也可以确定修改过的中断编号。
由于需修改内容较多,且实现逻辑并不复杂,因此这里仅仅提供一个思路,就不具体编写出来啦~
7. 章节总结
这一章简单讲解了外中断的工作原理及我们如何获取和利用中断信息实现一些自定义功能
通过这一章的练习,我们应该做到
- 理解CPU响应外部中断的大致工作原理及部分细节
- 能够简单利用中断例程在保留原有功能的基础上实现自定义功能
祝大家在计算机的学习之路上前进得坚定而顺利,不留遗憾,每天都有收获~~~