本页目录

【汇编语言】中断及外部设备操作

描述内存单元的标号

汇编语言中同样可以用标号表示内存单元,称为数据标号,使用起来的感觉很像C语言的数组。下面的程序计算arr中元素的和,并存放在数据标号x对应的字空间中:

assume cs:codesg,ds:datasg

datasg segment
    arr    db 1,2,3,4,5,6,7,8
    x      dw 0
datasg ends

codesg segment
    start:
           mov  ax,datasg
           mov  ds,ax

           mov  si,0
           mov  cx,8
    s:     mov  ah,0
           mov  al,arr[si]    ;使用数据标号
           add  x,ax          ;使用数据标号
           inc  si
           loop s

           mov  ax,4c00h
           int  21h
codesg ends
end start

和前面学习的地址标号相比,地址标号仅仅表示一个地址,而数据标号表示地址的同时还暗含了数据的类型,操作时不再需要用byte ptr等显式地指定数据的类型。

操作显存

屏幕上的内容其实就是显存中的数据。显存是一块内存区域,在8086 CPU中,显存占用地址a0000h~bffffh128KB的地址空间,而显示字符时常用到其中的一小块区域:b8000h~bffffh32KB的地址空间,它是25行、80列彩色字符模式第0页的显示缓冲区。

32KB的地址空间被划分为8个页,每个页4KB25行、80列一共2000个字符,每个字符用2个字节描述,刚好是一页的大小。这两个字节分别是:要显示符号的ASCII码、显示属性。

显示属性一个字节共8位,从高到低依次是:

7

6

5

4

3

2

1

0

含义

闪烁

背景R

背景G

背景B

高亮

前景R

前景G

前景B

直接修改内存对应地址的值,屏幕上就会立即有变化:

img

中断、中断处理程序

在8086 CPU中,中断可以打断当前程序的执行,当CPU收到中断请求时,会执行中断处理程序,处理完后再返回到原程序继续执行。中断处理程序的入口地址存放在中断向量表中,根据中断类型码查表得到对应处理程序的入口地址。

8086 CPU的中断向量表是一个1KB的表,包含了256个中断类型码对应的中断处理程序的入口地址,每个入口地址占4个字节,前两字节存放IP的值,后两字节存放CS的值。中断向量表的起始地址是00000h,终止地址是003ffh

00000h: 0号中断处理程序的入口地址IP
00002h: 0号中断处理程序的入口地址CS
00004h: 1号中断处理程序的入口地址IP
00006h: 1号中断处理程序的入口地址CS
00008h: 2号中断处理程序的入口地址IP
0000ah: 2号中断处理程序的入口地址CS
...
003fch: 255号中断处理程序的入口地址IP
003feh: 255号中断处理程序的入口地址CS

中断可以分为:

内部中断:由CPU内部产生,如除法错误(0号)、溢出(4号)、int n指令触发(n号)等。

外部中断:由外部设备产生,如键盘输入等。

img

其中外部中断又分为可屏蔽中断和不可屏蔽中断:

可屏蔽中断:CPU可以不响应的外中断,一般是由外部硬件通过INTR(Interrupt Request)信号线发送给CPU;CPU是否响应取决于IF标志位,如果IF=0,CPU不响应可屏蔽中断。

不可屏蔽中断:CPU必须响应的外中断,通过NMI(Non-Maskable Interrupt)信号线发送。8086 CPU不可屏蔽中断的中断类型码固定为2

几乎所有外部设备引发的中断都是可屏蔽中断,如键盘输入、打印机请求等;而不可屏蔽中断是在系统有必须处理的紧急情况发生时用来通知CPU的中断信息。

8086 CPU中断过程

中断过程由CPU的硬件自动完成,用中断类型码找到中断向量,并用它设置CS和IP。具体地说8086 CPU的中断过程为:

1.

从中断信息中取得中断类型码

2.

标志寄存器入栈(中断过程会改变标志,需要先进行保护)

3.

设置IF=0TF=0

4.

CS入栈,IP入栈

5.

从中断向量表读取中断处理程序的入口地址,设置CSIP

6.

开始执行中断处理程序

单步中断

标志寄存器中有两个标志位与中断有关:TF(Trap Flag)IF(Interrupt Flag)TF用于单步执行,IF用于可屏蔽中断的开关。

TF陷阱标志用于调试,当TF=1时,CPU在执行完一条指令后,会产生一个1号中断,由系统控制计算机。

IF中断标志用于控制CPU是否响应可屏蔽中断。当IF=1时,CPU响应可屏蔽中断;当IF=0时,CPU不响应可屏蔽中断。这个标志位可以用sti指令和cli指令来设置:

sti  ;设置IF=1,CPU响应可屏蔽中断
cli  ;设置IF=0,CPU不响应可屏蔽中断

讨论

8086 CPU中断过程第3步中,为什么要设置TF=0

中断处理程序也是一条条的指令。如果在执行中断处理程序前TF=1,则执行完程序的第一条指令后,又会产生单步中断,然后转去执行单步中断的中断处理程序。此时由于TF=1,则执行完第一条指令后,又会产生单步中断……

8086 CPU中断过程第3步中,为什么要设置IF=0

进入中断处理程序后,禁止其他的可屏蔽中断,避免中断嵌套。

int指令与iret指令

在执行int n时,逻辑上相当于自动依次执行了:pushfpush cspush ip;它和call指令保存CSIP的行为很像,但还保存并修改了标志寄存器的值。

对应地,从一个中断处理程序返回到原程序时,需要使用iret指令,逻辑上相当于自动依次执行了:pop ippop cspopf

int指令示例

在这里我们编写设计一个int 7ch中断,功能是将以0结尾的纯字母字符串转为大写,参数是DS:SI指向字符串首地址。

先看不使用中断,只用最一般的子程序调用的实现:

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
           db 'misunderstanding',0
datasg ends

stcksg segment
           db 16 dup(0)
stcksg ends

codesg segment
    start:
           mov  ax,datasg
           mov  ds,ax
           mov  si,0                       ;设置DS:SI为字符串首地址
           call i7ch

           mov  ax,4c00h
           int  21h

    i7ch:
           push cx
           push si
           mov  ch,0
    w:     mov  cx,[si]
           jcxz return
           and  byte ptr [si],11011111b
           inc  si
           jmp  w
    return:pop  si
           pop  cx
           ret
codesg ends
end start
img

现在希望编写一个i7ch中断处理程序,由int 7ch触发中断;此时需要一个确定、但又不影响系统的内存位置存放程序;一个技巧是使用中断向量表的内存区域,因为别的程序不会用到,而系统要处理的中断事件也远没有256个,因此可以利用中断向量表后段地址空间,这里选取从00200h开始的地址空间作为存放i7ch程序的目的地址。

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
              db 'misunderstanding',0
datasg ends

stcksg segment
              db 16 dup(0)
stcksg ends

codesg segment
       start:
       ;安装中断处理程序
                mov  ax,0
                mov  es,ax
                mov  di,200h                                ;设置安装位置,安装到ES:DI处
                call install
       ;设置中断向量表
                mov  word ptr es:[7ch*4],200h               ;设置7ch号中断的IP=200h
                mov  word ptr es:[7ch*4+2],0                ;设置7ch号中断的CS=0
       ;调用中断实现功能
                mov  ax,datasg
                mov  ds,ax
                mov  si,0                                   ;设置DS:SI为字符串首地址
                int  7ch

                mov  ax,4c00h
                int  21h

       ;中断安装程序
       ;使用movsb指令安装,该指令从DS:SI复制到ES:DI,ES:DI在前面已经设置好
       install:
                push ax
                push cx
                push si
                push ds
                cld                                         ;设置方向标志
                mov  ax,cs
                mov  ds,ax                                  ;设置DS为代码段起始地址
                mov  si,offset i7ch                         ;设置SI为offset i7ch(从子程序开始处复制)
                mov  cx,offset end_i7ch - offset i7ch       ;使用地址标号相减得到指令长度,也是循环次数
                rep  movsb
                pop  ds
                pop  si
                pop  cx
                pop  ax
                ret

       i7ch:
                push cx
                push si
                mov  ch,0
       w:       mov  cx,[si]
                jcxz return
                and  byte ptr [si],11011111b
                inc  si
                jmp  w
       return:  pop  si
                pop  cx
                iret
       end_i7ch:
                nop
codesg ends
end start

所谓的“安装程序”就是将i7ch中断处理程序的代码复制到内存中(通过movsb指令复制机器码),然后还需要修改中断向量表,将7ch号中断的入口地址设置为复制到的内存地址。这样触发7ch中断时就能够找到自定义的中断处理程序了。

执行的效果是相同的:数据段中的字符串会被改写为大写,但要注意此时中断处理程序已经写入了内存,在其他程序中可以直接调用int 7ch实现同样的功能!例如编写一个test程序:

assume cs:codesg,ds:datasg

datasg segment
              db 'internationalism',0
datasg ends

codesg segment
       start:
              mov ax,datasg
              mov ds,ax
              mov si,0
              int 7ch

              mov ax,4c00h
              int 21h
codesg ends
end start

假设前面的程序编译后得到i7ch.EXE,现在做一个实验,依次执行:

img

可以看到test程序中并没有7ch中断相关的逻辑,但先执行过i7ch.EXE之后,是可以调用相关逻辑完成转换大写功能的,说明成功在内存中存入程序、并改写入口地址了。

BIOS中断和DOS中断

BIOS中断调用示例

BIOS中断是由BIOS提供的一些服务程序,可以通过int指令调用。前面提到可以直接操作显存来控制屏幕显示的内容,而定位到具体的地址需要细节的计算;而BIOS的10h号中断提供了一系列关于字符显示的功能,包括设置光标位置、在指定位置显示字符等。BIOS中断相当于封装了底层的细节,提供了更方便的调用方式。

找到这些中断的中断号和功能描述,需要查手册!例如:

imgimgimg

这个手册指明显示服务的中断号是10h,我们如果想使用设置光标位置功能,需要设置四个输入参数:功能号AH,页号BH,行号DH,列号DL;显示字符功能同理。因此就可以编写出如下程序:

assume cs:codesg
codesg segment
    start:
           mov ah,2            ;设置光标位置功能号
           mov bh,0            ;页号为0
           mov dh,5            ;第5行
           mov dl,12           ;第12列
           int 10h             ;调用BIOS中断

           mov ah,9            ;显示字符功能号
           mov al,'a'          ;要显示的字符为'a'
           mov bl,01001010b    ;颜色属性
           mov bh,0            ;页号为0
           mov cx,3            ;重复3次,即显示3个'a'
           int 10h             ;调用BIOS中断

           mov ax,4c00h
           int 21h
codesg ends
end start
img

DOS中断

DOS中断是由操作系统提供的、更为高层的中断,同样提供了丰富的功能,使用时查手册即可。下图列出了一些DOS中断,并以21h为例做展开。

img

二者的联系

BIOS和DOS在所提供的中断处理程序中包含了许多子程序,这些子程序实现了编程时常用到的功能。
这些功能大多是调用外设的功能,而外设的硬件细节太多,常调用ROM中的BIOS中断来完成操作;
对于DOS中断来说,和硬件设备相关的DOS中断处理程序中,一般都是在操作系统级调用BIOS的中断处理程序来实现的,提供更加高层的一些功能。
当然如果这些都不能满足需求,用户也可以在程序里直接和外设进行联系(端口操作)。

img

端口的读写

CPU可以直接读写三个地方的数据:CPU内部的寄存器、内存单元、端口;而端口对应网卡、显卡等等外部芯片。这些外部芯片工作时,都有一些寄存器由CPU读写;而从CPU的角度,就把这些寄存器当作端口并统一编址。

端口的编址是16位的,范围是0~65535,这部分地址是独立于内存地址的。硬件设备与特定端口之间的映射是由硬件设计者决定的;为了确保不同厂商的设备能够正常工作,会有标准化组织制定统一的行业标准。

读写端口要使用in指令和out指令:

in  al,20h  ;从20h号端口读取一个字节到AL
out 21h,al  ;将AL的内容写入21h号端口

对于0~255号端口,端口号可以直接用立即数给出;对于256~65535号端口,端口号放在DX中:

mov  dx,3f8h
in   al,dx  ;从3f8h号端口读取一个字节到AL
out  dx,al  ;将AL的内容写入3f8h号端口

in指令和out指令中,只能使用AL(访问8位端口)或AX(访问16位端口)来存放读取或要写入的数据。

扬声器发声示例

下面的程序可以让扬声器响一下:

assume cs:codesg
codesg segment
    start:
           mov  al,0ffh
           out  42h,al       ;设置声音频率,第一次写入低8位
           mov  al,08h
           out  42h,al       ;设置声音频率,第二次写入高8位

           in   al,61h       ;读取端口原值
           mov  ah,al        ;保存原值到AH

           or   al,3         ;设置最低两位为1:打开扬声器和定时器
           out  61h,al       ;接通扬声器和定时器

           mov  cx,0ffffh
    delay: nop
           loop delay        ;延迟一段时间

           mov  al,ah
           out  61h,al       ;恢复端口原值

           mov  ax,4c00h
           int  21h
codesg ends
end start

键盘相关操作

键盘输入的处理过程

1.

键盘输入

键盘上每一个键相当于一个开关,键盘中有一个芯片对每一个键的开关状态进行扫描:按下/松开一个键时,芯片都会产生一个扫描码,扫描码被送入主板上相关接口芯片的寄存器中,该寄存器的端口地址为60h。扫描码与ASCII码不同,下图是通码:

img

按下一个键产生通码,松开一个键产生断码;通码的最高位为0,断码的最高位为1;通码和断码的低7位是相同的。

2.

引发int 9中断

键盘的输入到达60h端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果IF=1,则响应中断,转去执行int 9中断处理程序。

3.

执行int 9中断处理程序

BIOS键盘缓冲区

BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9中断处理程序所接收的键盘输入的内存区。BIOS键盘缓冲区可以存储最多15个键盘输入,每个键盘输入用一个字存放,高位字节存放扫描码,低位字节存放ASCII码。

如果输入了控制键和切换键,内存中有一个特殊的字节:键盘状态字节,地址是00417h,用于存放控制键和切换键的状态信息。字节内容为:

7

6

5

4

3

2

1

0

含义

Insert

Caps Lock

Num Lock

Scroll Lock

Alt

Ctrl

Shift

Shift

程序读出60h端口中的扫描码,如果是字符键的扫描码,将该扫描码和它所对应的ASCII码送入内存中的BIOS键盘缓冲区;如果是控制键和切换键的扫描码,则更新键盘状态字节。

更多中断操作

img

硬件中断、BIOS中断、DOS中断是由底层至上层针对键盘操作提供的不同功能。

以BIOSint 16h中断为例,第0号功能的过程是:

1.

检查键盘缓冲区是否有数据

2.

如果没有,重复第1

3.

读取缓冲区第一个字单元的键盘输入

4.

将读取的扫描码送入AH,ASCII码送入AL

5.

将已读取的键盘输入从缓冲区中删除

int 16h中断和int 9中断是一对相互配合的程序,int 9中断处理程序向键盘缓冲区中写入,int 16h中断处理程序从缓冲区中读出。它们写入和读出的时机不同,int 9中断处理程序在有键按下的时候向键盘缓冲区中写入数据;而int 16h中断处理程序是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。

练习

编写除法错误的中断处理程序

编程自定义除法错误中断,除以0时在屏幕上提示Cannot divide by zero!然后返回操作系统。

;以下代码会触发除法错误中断
mov  ax,1
mov  bl,0
div  bl

注:除法错误不仅仅是除以0,还包括除数溢出等情况。

选取从00200h开始的地址空间作为存放div0程序的目的地址。同时,我们希望中断处理程序能够永久驻留内存,因此为了保证正常工作,提示字符串的值也需要随程序一起复制到内存中,我们把这部分数据保存到00200h之前的32个字节(从001e0开始)。

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
           db 'Cannot divide by zero!',32 dup(0)
datasg ends

stcksg segment
           db 16 dup(0)
stcksg ends

codesg segment
    start:
    ;安装中断处理程序
             mov  ax,0
             mov  es,ax
             mov  di,200h-32                          ;设置安装位置,安装到ES:DI处,留出了32字节的数据空间用于存储提示字符串
             call install
    ;设置中断向量表
             mov  word ptr es:[0],200h                ;设置0号中断的IP=200h,真正的程序从200h开始
             mov  word ptr es:[2],0                   ;设置0号中断的CS=0

             mov  ax,4c00h
             int  21h

    ;中断安装程序
    ;使用movsb指令安装,该指令从DS:SI复制到ES:DI,ES:DI在前面已经设置好
    install:
             push ax
             push cx
             push si
             push ds
             cld                                      ;设置方向标志
    ;前32字节安装字符串常量
             mov  ax,datasg
             mov  ds,ax                               ;设置DS为数据段起始地址
             mov  si,0                                ;设置SI为0
             mov  cx,32                               ;一共复制32个字节(字符串+填充的0),循环32次
             rep  movsb
    ;后面安装程序
             mov  ax,cs
             mov  ds,ax                               ;设置DS为代码段起始地址
             mov  si,offset div0                      ;设置SI为offset div0(从子程序开始处复制)
             mov  cx,offset end_div0 - offset div0    ;使用地址标号相减得到指令长度,也是循环次数
             rep  movsb
             pop  ds
             pop  si
             pop  cx
             pop  ax
             ret

    ;中断处理程序
    div0:
             push ax
             push cx
             push si
             push di
             push ds
             push es
             mov  ax,0
             mov  ds,ax
             mov  si,200h-32                          ;初始化DS:SI=00200h-32,这是安装好的字符串数据的起始地址
             mov  ax,0b800h
             mov  es,ax
             mov  di,80*2*12                          ;初始化ES:DI=b8000h+80*2*12,这目标显存的起始地址(第12行开头)
             mov  ch,0
    w:       mov  cl,[si]                             ;字符串循环写入显存
             jcxz return                              ;遇到0就停止
             mov  es:[di],cl
             mov  byte ptr es:[di+1],40h              ;红底黑字
             inc  si
             add  di,2
             jmp  w
    return:  pop  es
             pop  ds
             pop  di
             pop  si
             pop  cx
             pop  ax
             mov  ax,4c00h
             int  21h                                 ;直接返回操作系统
    end_div0:
             nop
codesg ends
end start

编译上面的程序,命名为div0.EXE;接下来编译一个触发除法错误中断的程序,命名为bad.EXE

assume cs:codesg
codesg segment
    start: mov ax,1
           mov bl,0
           div bl

           mov ax,4c00h
           int 21h
codesg ends
end start

依次执行,在div0.EXE执行完成返回后,执行bad.EXE能够触发自定义的中断处理程序并在屏幕上输出提示内容。

img

自定义键盘输入处理

下面的程序可以在屏幕的中心以红底黑字依次显示字符A~Z,然后结束程序。子程序delay的作用是控制相邻两个字符显示的时间间隔。

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
           db 32 dup(0)
datasg ends

stcksg segment
           db 16 dup(0)
stcksg ends

codesg segment
    start:
    ;一段循环在屏幕中间显示A~Z的程序
           mov  ax,0b800h
           mov  es,ax
           mov  al,'A'
           mov   byte ptr es:[80*2*12+40*2+1],40h   ;第12行、第40列,红底黑字
    s:     mov  es:[80*2*12+40*2],al                ;显示字符
           call delay
           inc  al
           cmp  al,'Z'
           jng  s

           mov  ax,4c00h
           int  21h

    ;执行双重空循环,延时
    delay:
           push cx
           mov  cx,10h
    s1:    push cx
           mov  cx,0ffffh
    s2:    loop s2
           pop  cx
           loop s1
           pop  cx
           ret
codesg ends
end start

现在希望加入一个功能,在程序执行的任何时刻,按下Alt键可以切换字符的显示背景。(在红/绿色之间切换即可,只需要每次对显示属性字节异或01100000b

与上一题不同的是,首先本题不需要安装中断处理程序,只需要程序运行时能够定制化键盘中断的功能;另外由于键盘输入的中断处理程序涉及到所有键的输入,因此不能像上题一样完全重写一个处理程序,而是大致的思路应该为在原有9号中断处理程序的基础上,“插入”一个条件判断,如果是Alt键则执行想要的功能。

综上,本题采用的思路为,先把原来的中断向量保存到内存中,然后修改原来的中断向量指向自定义的中断处理程序i9;在返回前还要恢复原来的中断向量。而在子程序i9中,还要想办法调用原来的中断处理程序,正常响应键盘输入。一个大致的框架为:

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
           db 32 dup(0)
datasg ends

stcksg segment
           db 16 dup(0)
stcksg ends

codesg segment
    start:
    ;保存原来的中断向量到DS:0处
           mov   ax,datasg
           mov   ds,ax
           mov   ax,0
           mov   es,ax
           push  es:[9*4]
           pop   ds:[0]
           push  es:[9*4+2]
           pop   ds:[2]
    ;更改原来的中断向量
           mov   word ptr es:[9*4],offset i9               ;设置9号中断的IP为offset i9
           mov   es:[9*4+2],cs                             ;设置9号中断的CS为代码段起始地址

    ;一段循环在屏幕中间显示A~Z的程序
           ;...

    ;恢复原来的中断向量
           mov   ax,0
           mov   es,ax
           push  ds:[0]
           pop   es:[9*4]
           push  ds:[2]
           pop   es:[9*4+2]

           mov   ax,4c00h
           int   21h

    i9:
           ;...
codesg ends
end start

前面提到8086 CPU的中断过程为:

1.

从中断信息中取得中断类型码

2.

标志寄存器入栈(中断过程会改变标志,需要先进行保护)

3.

设置IF=0TF=0

4.

CS入栈,IP入栈

5.

从中断向量表读取中断处理程序的入口地址,设置CSIP

6.

开始执行中断处理程序

我们现在要在i9子程序中调用原来的int 9中断处理程序。请注意,程序之所以跳转到i9,是因为程序已经修改了中断向量表,然后由键盘输入产生int 9中断,中断过程的第1~5步已经执行完成,在第5步进入到i9子程序中;

而我们还想在i9子程序中手动调用被保存在dword ptr ds:[0]中的原来的int 9中断处理程序,此时没有人为我们自动执行中断过程了,需要手动模拟!因此这部分过程为:

1.

模拟标志寄存器入栈

pushf
2.

模拟设置IF=0TF=0

pushf
pop   bx                ;标志寄存器的值临时赋值给BX
and   bh,11111100b      ;IF=0,TF=0
push  bx
popf                    ;将更新后的值赋值给标志寄存器
3.

CS入栈、IP入栈、跳转到入口地址由call指令完成:

call  dword ptr ds:[0]

在原中断处理程序调用完成后,就是自定义的部分:

;自定义的键盘输入处理
in    al,60h                                    ;读取键盘输入送入AL
cmp   al,38h                                    ;判断是否为Alt键
jne   return                                    ;不是则返回
mov   ax,0b800h
mov   es,ax
xor   byte ptr es:[80*2*12+40*2+1],01100000b    ;在红色和绿色背景之间切换

注意因为这是中断调用,要使用iret指令返回。i9子程序的完整代码为:

i9:
       push  ax
       push  bx
       push  es
;------------------------------------------------------
;模拟标志寄存器入栈
       pushf
;模拟设置IF=0,TF=0
       pushf
       pop   bx
       and   bh,11111100b
       push  bx
       popf
;call指令即可实现CS、IP入栈并跳转到入口地址
       call  dword ptr ds:[0]
;------------------------------------------------------
;自定义的键盘输入处理
       in    al,60h                                    ;读取键盘输入送入AL
       cmp   al,38h                                    ;判断是否为Alt键
       jne   return                                    ;不是则返回
       mov   ax,0b800h
       mov   es,ax
       xor   byte ptr es:[80*2*12+40*2+1],01100000b    ;在红色和绿色背景之间切换
return:
       pop   es
       pop   bx
       pop   ax
       iret

题目的完整代码为:

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
           db 32 dup(0)
datasg ends

stcksg segment
           db 16 dup(0)
stcksg ends

codesg segment
    start:
    ;保存原来的中断向量到DS:0处
           mov   ax,datasg
           mov   ds,ax
           mov   ax,0
           mov   es,ax
           push  es:[9*4]
           pop   ds:[0]
           push  es:[9*4+2]
           pop   ds:[2]
    ;更改原来的中断向量
           mov   word ptr es:[9*4],offset i9               ;设置9号中断的IP为offset i9
           mov   es:[9*4+2],cs                             ;设置9号中断的CS为代码段起始地址
    ;一段循环在屏幕中间显示A~Z的程序
           mov   ax,0b800h
           mov   es,ax
           mov   al,'A'
           mov   byte ptr es:[80*2*12+40*2+1],40h          ;第12行、第40列,红底黑字
    s:     mov   es:[80*2*12+40*2],al                      ;显示字符
           call  delay
           inc   al
           cmp   al,'Z'
           jng   s
    ;恢复原来的中断向量
           mov   ax,0
           mov   es,ax
           push  ds:[0]
           pop   es:[9*4]
           push  ds:[2]
           pop   es:[9*4+2]

           mov   ax,4c00h
           int   21h

    ;执行双重空循环,延时
    delay:
           push  cx
           mov   cx,10h
    s1:    push  cx
           mov   cx,0ffffh
    s2:    loop  s2
           pop   cx
           loop  s1
           pop   cx
           ret

    i9:
           push  ax
           push  bx
           push  es
    ;------------------------------------------------------
    ;模拟标志寄存器入栈
           pushf
    ;模拟设置IF=0,TF=0
           pushf
           pop   bx
           and   bh,11111100b
           push  bx
           popf
    ;call指令即可实现CS、IP入栈并跳转到入口地址
           call  dword ptr ds:[0]
    ;------------------------------------------------------
    ;自定义的键盘输入处理
           in    al,60h                                    ;读取键盘输入送入AL
           cmp   al,38h                                    ;判断是否为Alt键
           jne   return                                    ;不是则返回
           mov   ax,0b800h
           mov   es,ax
           xor   byte ptr es:[80*2*12+40*2+1],01100000b    ;在红色和绿色背景之间切换

    return:
           pop   es
           pop   bx
           pop   ax
           iret
codesg ends
end start

执行效果(按下Alt键切换背景颜色):

img