12.2 中断处理函数

CPU收到中断信息后,需要对中断信息进行处理。用来处理中断信息的程序称为中断处理程序。

8086CPU中,需要修改CS:IP执行中断处理程序的段地址和偏移地址,如何获得段地址和偏移地址后面介绍。

12.3 中断向量表

CPU用8位的中断类型码通过中断向量表找到对应的中断处理程序的入口地址。

中断向量表就是中断向量的列表,中断向量就是中断处理程序的入口地址,中断向量表就是中断处理程序入口地址的列表。

中断向量表在内存中保存,其中存放着256个中断源所对应的中断处理程序的入口。

CPU只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位对应的表项,从而得到中断处理程序的入口地址。

对于8086CPU,中断向量表指定放在内存地址0处,从内存0000:0000到0000:03FF的1024个单元中存放着中断向量表。

对于8086CPU,一个表项存放一个中断向量,也就是一个中断处理程序的入口地址,包括段地址和偏移地址,所以一个表项占两个字,高地址存放段地址,低地址存放偏移地址。

(IP)=(N*4),(CS)=(N*4+2),N为中断类型码。

12.4 中断过程

在中断向量表中找到中断处理程序的入口,目的是设置CS和IP,使CPU执行中断处理程序。

用中断类型码找到中断向量,并用来设置CS和IP,这个过程是由CPU自动完成,称为中断过程。硬件在完成中断过程后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理程序。

8086CPU在收到中断信息后,所引发的中断过程如下:

  • 从中断信息中取得中断信息码;
  • 标志寄存器的值入栈(中断过程中要改变标志寄存器的值);
  • 设置标志寄存器的第8位T-Flag(trap flag)和第9位I-Flag(interrupt flag)的值为0;
  • CS的内容入栈;
  • IP的内容入栈;
  • 从内存地址为中断类型码*4和中断类型码*4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS。
  • 使用汇编语言描述:

  • 取得中断类型码N;
  • pushf
  • T-Flag=0,I-Flag=0
  • push CS
  • push IP
  • (IP)=(N*4),(CS)=(N*4+2)
  • 12.5 中断处理程序和iret指令

    CPU随时都可能检测到中断信息,CPU随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存某段空间之中。

    中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。

    中断处理程序编写的步骤:

  • 保存用到的寄存器;
  • 处理中断;
  • 恢复用到的寄存器;
  • 用iret指令返回。
  • iret指令的功能用汇编语法描述为:

    pop IP
    pop CS
    

    12.6 除法错误中断的处理

    除法错误中断(0号中断)。当CPU执行div等除法指令的时候,如果发生了除法溢出错误,将产生中断类型码为0的中断信息,CPU检测到这个信息,然后引发中断过程,转去执行0号中断所对应的中断处理程序。

    实验:编写一个0号中断处理程序,在屏幕中间显示“overflow!”,然后返回系统。

    要求:do0子程序应该放在内存的确定位置。重新找个地方,不破坏系统。

    简便方案:绕过操作系统,直接找一块别的程序不会用到的内存区,将中断处理程序do0传送到其中。

    实现:利用中断向量表的空闲单元来存放do0,估计不会超过256字节,选择0000:0200~0000:02FF这段空间。中断处理程序的段地址放在0*4+2字单元中,偏移地址放在0*4字单元中。

    程序框架:

    assume cs:code
    code segment
    start:
    	do0安装程序		; 将do0送入内存0000:0200处
    	设置中断向量表	  ; 设置0号中断表项
    	mov ax, 4c00h
    	int 21h
    	显示字符串"overflow!"
    	mov ax, 4c00h
    	int 21h
    code ends
    end start
    

    12.8 安装

    可以使用movsb指令,将do0的代码送入0:200处:

    assume cs:code
    code segment
    start:
    	设置es:di指向目的地址
    	设置ds:si指向源地址
    	设置cx为传输长度
    	设置传输方向为正
    	rep movsb
    	设置中断向量表
    	mov ax, 4c00h
    	int 21h
    	显示字符串"overflow!"
    	mov ax, 4c00h
    	int 21h
    code ends
    end start
    

    用rep movsb指令需要确认的信息:

  • 传送的原始地址,段地址:code,偏移地址:offset do0;
  • 传送的目的位置:0:200;
  • 传送的长度:do0部分代码的长度;
  • 传送的方向:正向。
  • assume cs:code
    code segment
    start:
    	mov ax, cs
    	mov ds, ax
    	mov si, offset do0	; 设置ds:si指向源地址
    	mov ax, 0
    	mov es, ax
    	mov di, 200h		; 设置es:di指向目的地址
    	mov cx, do0 部分代码的长度	; 设置cx为传输长度
    	cld		; 设置传输方向为正
    	rep movsb
    	设置中断向量表
    	mov ax, 4c00h
    	int 21h
    	显示字符串"overflow!"
    	mov ax, 4c00h
    	int 21h
    code ends
    end start
    

    do0代码的长度如何获取?可以利用编译器来计算do0的长度。

    assume cs:code
    code segment
    start:
    	mov ax, cs
    	mov ds, ax
    	mov si, offset do0	; 设置ds:si指向源地址
    	mov ax, 0
    	mov es, ax
    	mov di, 200h		; 设置es:di指向目的地址
    	mov cx, offset do0end-offset do0 ; 设置cx为传输长度,'-'编译器可识别为减法
    	cld		; 设置传输方向为正
    	rep movsb
    	设置中断向量表
    	mov ax, 4c00h
    	int 21h
    	显示字符串"overflow!"
    	mov ax, 4c00h
    	int 21h
    do0end: nop
    code ends
    end start
    

    12.9 do0子程序

    do0程序的任务是显示字符串:

    设置ds:si指向字符串 mov ax, 0b800h mov es, ax mov di, 12*160+36*2 ; 设置es:di指向显存空间的中间位置 mov cx, 9 ; 设置cx为字符串长度 s: mov al, [si] mov es:[di], al inc si add di, 2 loop s mov ax, 4c00h int 21h do0end: nop
    assume cs:code
    data segment
    	db 'overflow!'
    data ends
    code segment
    start:
    	mov ax, cs
    	mov ds, ax
    	mov si, offset do0	; 设置ds:si指向源地址
    	mov ax, 0
    	mov es, ax
    	mov di, 200h		; 设置es:di指向目的地址
    	mov cx, offset do0end-offset do0 ; 设置cx为传输长度,'-'编译器可识别为减法
    	cld		; 设置传输方向为正
    	rep movsb
    	设置中断向量表
    	mov ax, 4c00h
    	int 21h
    	mov ax, data
    	mov ds, ax
    	mov si, 0	; 设置ds:si指向字符串
    	mov ax, 0b800h
    	mov es, ax
    	mov di, 12*160+36*2	; 设置es:di指向显存空间的中间位置
    	mov cx, 9			; 设置cx为字符串长度
    s:	mov al, [si]
    	mov es:[di], al
    	inc si
    	add di, 2
    	loop s
    	mov ax, 4c00h
    	int 21h
    do0end: nop
    code ends
    end start
    

    上述代码的问题,程序执行后,data段占用的内存空间会被系统释放,而在其中存放的“overflow!”可能被覆盖。所以,字符串应该存放在一段不会被覆盖的空间中。

    assume cs:code
    code segment
    start:
    	mov ax, cs
    	mov ds, ax
    	mov si, offset do0	; 设置ds:si指向源地址
    	mov ax, 0
    	mov es, ax
    	mov di, 200h		; 设置es:di指向目的地址
    	mov cx, offset do0end-offset do0 ; 设置cx为传输长度,'-'编译器可识别为减法
    	cld		; 设置传输方向为正
    	rep movsb
    	设置中断向量表
    	mov ax, 4c00h
    	int 21h
    	jmp short do0start
    	db "overflow!"
    do0start:
    	mov ax, cs
    	mov ds, ax
    	mov si, 202h	; 设置ds:si指向字符串
    	mov ax, 0b800h
    	mov es, ax
    	mov di, 12*160+36*2	; 设置es:di指向显存空间的中间位置
    	mov cx, 9			; 设置cx为字符串长度
    s:	mov al, [si]
    	mov es:[di], al
    	inc si
    	add di, 2
    	loop s
    	mov ax, 4c00h
    	int 21h
    do0end: nop
    code ends
    end start
    

    12.10 设置中断向量

    将do0的入口地址0:200,写入中断向量表的0号表项中。

    0号表项地址为0:0,其中0:0字单元存放偏移地址,0:2字单元存放段地址。

    mov ax, 0
    mov es, ax
    mov word ptr es:[0*4], 200h	; (IP)=200H
    mov word ptr es:[0*4+2], 0	; (CS)=0H
    

    完整的程序:

    assume cs:code
    code segment
    start:
    ; do0安装程序
    	mov ax, cs
    	mov ds, ax
    	mov si, offset do0	; 设置ds:si指向源地址
    	mov ax, 0
    	mov es, ax
    	mov di, 200h		; 设置es:di指向目的地址
    	mov cx, offset do0end-offset do0 ; 设置cx为传输长度,'-'编译器可识别为减法
    	cld		; 设置传输方向为正
    	rep movsb
    ; 设置中断向量表
        mov ax, 0
        mov es, ax
        mov word ptr es:[0*4], 200h	; (IP)=200H
        mov word ptr es:[0*4+2], 0	; (CS)=0H
    	mov ax, 4c00h
    	int 21h
    ; 显示字符串"overflow"
    	jmp short do0start
    	db "overflow!"
    do0start:
    	mov ax, cs
    	mov ds, ax
    	mov si, 202h	; 设置ds:si指向字符串
    	mov ax, 0b800h
    	mov es, ax
    	mov di, 12*160+36*2	; 设置es:di指向显存空间的中间位置
    	mov cx, 9			; 设置cx为字符串长度
    s:	mov al, [si]
    	mov es:[di], al
    	inc si
    	add di, 2
    	loop s
    	mov ax, 4c00h
    	int 21h
    do0end: nop
    code ends
    end start
    

    12.11 单步中断

    两个和中断相关的寄存器标志位:

  • T-Flag(trap flag)陷阱标志:用于调试时的单步方式操作,当T-Flag=1时,每条指令执行完后产生缺陷,由系统控制计算机;当T-Flag=0时,CPU正常工作,不产生陷阱。
  • I-Flag(interrupt flag)中断标志:当I-Flag=1时,允许CPU响应可屏蔽中断请求;当I-Flag=0时,关闭中断。
  • CPU执行完一条指令之后,如果检测到标志寄存器的T-Flag位为1,则产生单步中断,引发中断过程,执行中断处理程序。

    单步中断的中断类型码为1,它引发的中断过程如下:

  • 取得中断类型码1;
  • 标志寄存器入栈,T-Flag、I-Flag设置为0;
  • CS、IP入栈;
  • (IP)=(1*4),(CS)=(1*4+2)。
  • 如果在执行中断处理程序之前,T-Flag=1,则CPU在执行完中断处理程序的第一条指令后,又要产生单步中断,转去执行单步中断的中断处理程序的第一条指令。

    上面的过程将陷入一个永远不能结束的循环,CPU永远执行单步中断处理程序的第一条指令,所以,在进入中断处理程序之前,设置T-Flag=0。

    12.12 响应中断的特殊情况

    一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就响应中断,引发中断过程。

    可是在有些情况,CPU在执行完当前指令后,即便发生中断,也不会响应。

    举例:在执行完向ss寄存器传送数据的指令后,即便发生中断,CPU也不会响应。

    原因:ss:sp联合指向栈顶,而对它们的设置应该连续完成,以此保证对栈的正确操作。

    13 int指令

    本章介绍由int指令引发的中断。

    13.1 int指令

    功能:引发中断过程。

    指令格式:

    int n	; n为中断类型码
    

    CPU执行int n指令,相当于引发一个n号中断的中断过程,执行过程如下:

  • 取中断类型码n;
  • 标志寄存器入栈,I-Flag=0,T-Flag=0;
  • CS、IP入栈;
  • (IP)=(n*4),(CS)=(n*4+2)。
  • 从此处转去执行n号中断的中断处理程序。

    assume cs:code
    start:
    	mov ax, 0b800H
    	mov byte ptr es:[12*160+40*2], '!'
    	int 0	; 引发0号中断,执行0号中断处理程序
    code ends
    end start
    

    一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。在编程的时候,可以用int指令调用这些子程序。

    13.2 编写供应用程序调用的中断例程

    实验:编写、安装中断7cH的中断例程。

    功能:求一word型数据的平方。

    参数:(ax)=要计算的数据。

    返回值:dx、ax中存放结果的高16位和低16位。

    assume cs:code
    code segment
    start:
    	mov ax, 3456	; 输入参数
    	int 7ch			; 调用中断7ch的中断例程,计算ax中的数据的平方
    	add ax, ax
    	adc dx, ax		; dx:ax存放结果,将结果乘以2
    	mov ax, 4c00h
    	int 21h
    code ends
    end start
    

    编写安装程序的步骤:

  • 编写实现求平方功能的程序;
  • 安装程序,将其安装在0:200处;
  • 设置中断向量表,将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程。
  • assume cs:code
    code segment
    start:
    	mov ax, cs
    	mov ds, ax
    	mov si, offset sqr	; 设置ds:si指向源地址
    	mov ax, 0
    	mov es, ax
    	mov di, 200h		; 设置es:si指向目的地址
    	mov cx, offset sqrend-offset sqr
    	rep movsb
    	; 设置中断向量
    	mov ax, 0
    	mov es, ax
    	mov word ptr es:[7ch*4], 200h
    	mov word ptr es:[7ch*4+2], 0
    	mov ax, 4c00h
    	int 21h
    	mul ax
    sqrend:
    code ends
    end start
    

    13.4 BIOS和DOS所提供的中断例程

    系统板的ROM中存放着一段程序,称为BIOS(基本输入输出系统),主要包含以下几部分内容:

  • 硬件系统的检测和初始化程序;
  • 外部中断和内部中断的中断例程;
  • 用于对硬件设备进行I/O操作的中断例程;
  • 其它和硬件系统相关的中断例程。
  • 操作系统DOS也提供了中断例程,DOS中断例程就是操作系统向程序员提供的编程资源。

    程序员在编程的时候,可以用int指令直接调用BIOS和DOS提供的中断例程。

    13.5 BIOS和DOS中断例程的安装过程

    BIOS和DOS提供的中断例程安装过程:

  • 开机后,CPU一加电,初始化(CS)=0FFFFH,(IP)=0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条转移指令,CS执行该指令后,转去执行BIOS中的硬件系统检测的初始化程序。
  • 初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。对于BIOS所提供的中断例程,只需将入口地址等级在中断向量表中,因为它们是固化到ROM中的程序,一直在内存中存在。
  • 硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
  • DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
  • 13.6 BIOS中断例程应用

    BIOS提供了很多中断例程,BIOS中断类别:

  • 显示服务(Video Service,INT 10H)
  • 直接磁盘服务(Direct Disk Service,INT 13H)
  • 串行口服务(Serial Port Service,INT 14H)
  • 杂项系统服务(Miscellaneous System Service,INT 15H)
  • 键盘服务(KeyBoard Service,INT 16H)
  • 并行口服务(Parallel Port Service,INT 17H)
  • 时钟服务(Clock Service,INT 1AH)
  • 直接系统服务(Direct System Service)
  • BIOS中断例程可参考:BIOS和DOS中断大全 - 无上至尊自然妙有弥罗 - 博客园 (cnblogs.com)

    一般程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。BIOS和DOS提供的中断例程,都用ah来传递内部子程序的编号。

    实验:使用int 10h中断例程设置光标位置

    实现:需要用到编号02H(设置光标位置)和

    功能描述:用文本坐标下设置光标位置
    入口参数:AH=02H
    BH=显示页码
    DH=行(Y坐标),0~24
    DL=列(X坐标),0~79
    出口参数:无
    功能描述:在当前光标处按指定属性显示字符
    入口参数:AH=09H
    AL=字符
    BH=显示页码
    BL=属性(文本模式)或颜色(图形模式)
    CX=重复输出字符的次数
    出口参数:无
    

    页号的含义:8086CPU显存地址空间为:A0000H~BFFFFH,总共128KB。其中B8000H~BFFFFH共32KB的空间,是80*32(25行*80列)彩色字符模式第0页的显示缓冲区,一屏的内容在显示缓冲区中总共占4000个字节。

    显示缓冲区分为8页,每页4KB,显示器可以显示任意一页的内容,一般显示第0页的内容,也就是B8000H~B8F9FH中的4000个字节。

    功能号09H的参数bl显示的颜色属性格式为:

    实验代码:

    assume cs:code
    code segment
    mov ah, 2	; 设置光标
    mov bh, 0	; 第0页
    mov dh, 5	; 第5行
    mov dl, 12	; 第12列
    int 10h
    mov ah, 9
    mov al, 'a'	; 在光标位置显示字符
    mov bl, 11001010b	; 颜色属性
    mov bh, 0	; 第0页
    mov cx, 3	; 字符重复个数
    int 10h
    mov ax, 4c00h
    int 21h
    code ends
    

    13.7 DOS中断例程应用

    int 21h中断例程是DOS提供的中断例程,其中包含了DOS提供给程序员在编程时调用的子程序。

    DOS中断类别:

  • INT 20H:终止程序运行
  • INT 21H:功能调用
  • INT 22H:终止处理程序的地址
  • INT 23H:Ctrl+C处理程序
  • INT 24H:致命错误处理程序
  • INT 25H:读磁盘扇区(忽略逻辑结构)
  • INT 26H:写磁盘扇区(忽略逻辑结构)
  • INT 27H:终止,并驻留在内存
  • INT 28H:DOS空闲
  • INT 2FH:多重中断服务
  • INT 33H:鼠标功能
  • INT 21H功能常用调用函数分类:

  • 01H、07H和08H:从标准输入设备输入字符
  • 02H:字符输出
  • 03H:辅助设备的输入
  • 04H:辅助设备的输出
  • 05H:打印输出
  • 06H:控制台输入/输出
  • 09H :显示字符串
  • 0AH : 键盘缓冲输入
  • 0BH :检测输入状态
  • 0CH :清输入缓冲区的输入功能
  • 00H:终止进程
  • 26H:创建新的程序段前缀(PSP)
  • 31H :终止并驻留
  • 4BH :执行程序(EXEC)
  • 4CH :带返回码方式的终止进程
  • 4DH :读取返回代码
  • 62H :读取PSP地址
  • 前面一直使用的程序返回就是21H中断的4CH功能号,可以提供返回值作为参数:

    功能描述:终止程序的执行,并可返回一个代码
    入口参数:AH=4CH
    AL=返回的代码
    出口参数:无
    mov ax, 4c00h
    int 21h
    

    实验:在屏幕的第5行12列显示字符串”Welcome to masm!“。

    实现:需要使用到INT 21H中断的09H功能号

    功能描述:输出一个字符串到标准输出设备上。如果输出操作被重定向,那么,将无法判断磁盘已满。
    入口参数:AH=09H
    DS:DX=待输出字符的地址
    说明:待显示的字符串以’$’作为其结束标志
    出口参数:无
    
    assume cs:code
    data segment
    	db 'Welcome to masm!', '$'
    data ends
    code segment
    start:
    	mov ah, 2	; 设置光标
    	mov bh, 0	; 第0页
    	mov dh, 5	; 行号
    	mov dl, 12	; 列号
    	int 10h
    	mov ax, data
    	mov ds, ax
    	mov dx, 0;	ds:dx指向字符串首地址data:0
    	mov ah, 9
    	int 21h
    	mov ax, 4c00h
    	int 21h
    code ends
    end start
    

    14 端口

    PC系统中,除了和CPU通过总线直连的存储器外,还有很多外设,比如网卡、显卡、输入输出设备等。

    在这些芯片中,都有一组可以由CPU读写的寄存器,物理上是不同的芯片,但是有两点相同:

  • 都和CPU的总线相连。
  • CPU对它们进行读或写的时候都是通过控制线向它们所在的芯片发出端口读写命令。
  • 从CPU角度,将这些寄存器都当作端口,对它们进行统一编制,从而建立了一个统一的端口地址空间,每一个端口在地址空间中都有一个地址。

    CPU可以直接读写3个地方的数据:

  • CPU内部的寄存器;
  • 内存单元;
  • 14.1 端口的读写

    在访问端口的时候,CPU通过端口地址来定位芯片。端口地址和内存地址一样,通过地址总线来传送。PC系统中,CPU最多可以定位64KB个不同的端口,端口的地址范围为0~65535。

    端口的读写指令只有两条:in和out,分别用于从端口读取数据和往端口写入数据。

    CPU执行内存访问指令和端口访问指令时候,总线上的信息:

    (1)访问内存:

    mov ax, ds:[8]	; 假设(ds)=0
    

    执行时与总线相关的操作如下:

  • CPU通过地址线将地址信息8发出;
  • CPU通过控制线发出内存读命令,选中存储芯片,并通知它将要从中读取数据;
  • 存储器将8号单元中的数据通过数据线送入CPU。
  • (2)访问端口

    in al, 60h	; 从60h号端口读取一个字节
    

    执行时与总线相关的操作如下:

  • CPU通过地址线将地址信息60h发出;
  • CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它将要从中读取数据;
  • 端口所在的芯片将60H端口中的数据通过数据线送入CPU。
  • in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。访问8位端口时使用al,访问16位端口时使用ax。

    对0~255以内的端口进行读写时:

    in al, 20h	; 从20h端口读入一个字节
    out 20h, al	; 往20h端口写入一个字节
    

    对256~65535的端口进行读写时,端口号放在dx中:

    mov dx, 3f8h	; 将端口号3f8h送入dx
    in al, dx		; 从3f8h端口读入一个字节
    out dx, al		; 向3f8h端口写入一个字节
    

    14.2 CMOS RAM芯片

    PC机中,有一个CMOS RAM芯片,简称CMOS。此芯片的特征如下:

  • 包含一个实时钟和一个有128个存储单元的RAM存储器。
  • 该芯片靠电池供电,关机后其内部时钟仍可正常运行,RAM中信息不丢失。
  • 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用来保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM中的系统信息。
  • 该芯片内部有两个端口,端口地址为70h和71h,CPU通过这两个端口来读写CMOS RAM。
  • 70h为地址端口,存放要访问的CMOS RAM单元的地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入其中的数据。
  • CPU对CMOS RAM的读写分两步进行,比如读CMOS RAM的2号单元:

  • 将2送入端口地址70h;
  • 从端口71h读出2号单元的内容。
  • 14.3 CMOS RAM中存储的时间信息

    在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息的长度都为1个字节,存放单元为:

    秒:0	分:2	时:4	日:7	月:8	年:9
    

    这些数据以BCD码的方式存放,BCD码是以4位二进制数表示十进制数码的编码方法,如下:

    十进制数码

    比如,数值26,用BCD码表示为:0010 0110。

    一个字节表示两个BCD码。CMOS RAM存储时间信息的单元中,存储了用两个BCD码表示的两位十进制数,高4位的BCD码表示十位,低4位BCD码表示个位。比如,00010100b表示14。

    实验:显示从CMOS中读出的月份

    assume cs:code
    code segment 
    start:
    	; 从CMOS中读出月份的数据
    	mov al, 8
    	out 70h, al
    	in al, 71h
    	; 从BCD码中解析出月份的十位和个位
    	mov ah, al
    	mov cl, 4
    	shr ah, cl		  ; 存放十位
    	and al, 00001111b ; 存放个位
    	; 显示月份信息
    	add ah, 30h	; 转换为ASCLL码
    	add al, 30h	; 转换为ASCLL码
    	mov bx, 0b800h
    	mov es, bx
    	mov byte ptr es:[160*12+40*2], ah	; 显示月份的十位数码
    	mov byte ptr es:[160*12+40*2+2], al	; 显示月份的个位数码
    	mov ax, 4c00h
    	int 21h
    code ends
    

    15 外中断

    CPU可以对外部设备进行控制,接收它们的输入,向它们进行输出。也就是,CPU还有I/O(input/output,输入和输出)能力。

    要及时处理外设的输入,需要解决两个问题:①外设的输入随时可能产生,CPU如何得知;②CPU从何处得到外设的输入。

    15.1 接口芯片和端口

    PC系统装有各种接口芯片,这些外设接口芯片的内部有若干寄存器,CPU将这些寄存器当作端口来访问。

    外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。

    CPU还可以向外设输出控制命令,这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。

    15.2 外中断信息

    CPU通过中断机制来实时对随时发生的外设事件进行处理。当CPU外部有需要处理的事情发生时,相关芯片向CPU发出相应的中断信息,CPU在执行完当前的指令后,可以检测到发送过来的中断信息,引发中断过程,处理外部的中断请求。

    PC系统中,外中断可以分为两类:

    (1)可屏蔽中断

    可屏蔽中断是CPU可以不响应的中断。当CPU检测到可屏蔽中断信息时,如果标志寄存器的I-Flag=1,则CPU在执行完当前指令后响应中断;如果I-Flag=0,则不响应可屏蔽中断。

    可屏蔽中断信息来自CPU外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。

    在中断过程中将I-Flag置为0的原因是:在进入中断处理程序后,禁止其他的可屏蔽中断。

    可以用指令配置I-Flag,如下:

    sti	; 设置I-Flag=1
    cli	; 设置I-Flag=0
    

    (2)不可屏蔽中断

    不可屏蔽中断是CPU必须响应的中断,当CPU检测到不可屏蔽中断信息时,处理完当前指令后,立即响应,引发中断过程。

    8086CPU中,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码,不可屏蔽中断的中断过程为:

  • 标志寄存器入栈,I-Flag=0,T-Flag=0;
  • CS、IP入栈;
  • (IP)=8,(CS)=0AH。
  • 15.3 PC机键盘的处理过程

    通过键盘的输入处理过程,来学习PC机处理外设输入的基本方法。

    (1)键盘输入

    键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。

    按下一个键,开关接通,产生一个扫描码(称为通码),然后送入相关接口芯片中,该寄存器的端口地址为60H。松开一个键,产生的扫描码称为断码。扫描码长度是一个字节,通码的第7位为0,断码的第7位为1,断码=通码+80H。

    以下是一些键盘的扫描码,仅列出通码:

    (2)引发9号中断

    键盘的输入到达60H端口时,相关芯片向CPU发送中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果I-Flag=1,则响应中断,引发中断过程,转去执行int 9中断例程。

    (3)执行int 9中断例程

    BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要完成工作:

  • 读出60h端口中的扫描码;
  • 如果是字符键的扫描码,将该扫描码和它对应的字符码(ASCLL码)送入内存中的BIOS键盘缓冲区;如果是控制键和切换键的扫描码,则将其转变为状态字节写入内存中存储状态字节的单元。
  • 对键盘系统进行相关的控制。
  • BIOS键盘缓冲区用于存放int 9中断例程所接收的键盘输入的内存区,可以存储15个键盘的输入。一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。

    15.4 编写int 9中断例程

    键盘输入处理过程:

  • 键盘产生扫描码;(硬件完成)
  • 扫描码送入60h端口;(硬件完成)
  • 引发9号中断;(硬件完成)
  • CPU执行int 9中断例程处理键盘输入。
  • 实验:编程在屏幕中间依次显示'a'~'z',并可以让人看清,在显示的过程中,按下Esc键后,改变显示的颜色。

    实现:1)由于指令执行太快,应该在每个字母显示之后进行延时;2)编写int 9中断例程。

    int 9中断例程功能如下:

  • 从60h端口读出键盘的输入;
  • 调用BIOS的int 9中断例程,处理其他硬件细节;
  • 判断是否为Esc的扫描码,如果是,更改显示的颜色后返回;如果不是则直接返回。
  • 问题:BIOS提供的中断例程我们需要先进行调用,但是中断向量的地方又是我们新的中断例程的地址,因此我们可以将BIOS提供的中断例程入口地址存放到ds:0和ds:2单元中。

    完整程序:

    assume cs:code
    stack segment
    	db 128 dup(0)
    stack ends
    data segment
    	dw 0, 0
    data ends
    code segment
    start:
    	mov ax, stack
    	mov ss, ax
    	mov sp, 128	; 设置栈顶指针
    	mov ax, data
    	mov ds, ax	; data段
    	mov ax, 0
    	mov es, ax
    	push es:[9*4]
    	pop ds:[0]
    	push es:[9*4+2]
    	pop ds:[2]	; 将BIOS提供的int 9中断例程的入口地址保存在ds:0,ds:2单元
    	mov word ptr es:[9*4], offset int9
    	mov es:[9*4+2], cs	; 在中断向量表中设置新的int 9中断例程的入口地址
    	; 显示程序
    	mov ax, 0b800h
    	mov es, ax
    	mov ah, 'a'
    s:	mov es:[160*12+40*2], ah
    	call delay	; 延时程序
    	inc ah
    	cmp ah, 'z'
    	jna s
    	mov ax, 0
    	mov es, ax
    	push ds:[0]
    	pop  es:[9*4]
    	push ds:[2]
    	pop  es:[9*4+2]	; 将中断向量表中int 9中断例程的入口地址恢复为原来的地址
    	mov ax, 4c00h
    	int 21h
    delay:	; 延时函数,循环100000h次,可根据实际调整
    	push ax
    	push dx
    	mov dx, 1000h
    	mov ax, 0
    s1:	sub ax, 1
    	sbb dx, 0
    	cmp ax, 0
    	jne s1
    	cmp dx, 0
    	jne s1
    	pop dx
    	pop ax
    int9:	; 新的int 9中断例程
    	push ax
    	push bx
    	push es
    	in al, 60h	; 从端口地址60H中读取数据
    	pushf	; 用于调用新的中断例程
    	pushf	; 用于调用原来的int9中断例程
    	pop bx
    	and bh, 11111100b	; 设置标志寄存器I-Flag=0,T-Flag=0
    	push bx
    	popf	; 关闭I-Flag和T-Flag
    	call dword ptr ds:[0]	; 对int指令进行模拟,调用原来的int 9中断例程。
    	cmp al, 1	; Esc扫描码(BCD码)为01
    	jne int9ret
    	mov ax, 0b800h
    	mov es, ax
    	inc byte ptr es:[160*12+40*2+1]	; 属性值加1,改变颜色
    int9ret:
    	pop es
    	pop bx
    	pop ax
    code ends
    end start
    

    15.5 安装新的int 9中断例程

    实验:安装新的int 9中断例程,扩展原有的int 9中断例程。

    功能:在DOS下,按F1键后改变当前屏幕的颜色,其他的键照常处理。

    实现:1)改变屏幕颜色,改变从内存地址B8000H开始的4000个字节中的所有基地址单元中的内容,当前屏幕颜色将变化。2)其它键照常处理,调用原来的int 9中断处理例程。3)原int9中断例程入口地址的保存,保存在0:200单元处(安装程序返回后不丢失)。

    完整程序:

    assume cs:code
    stack segment
    	db 128 dup(0)
    stack ends
    code segment
    start:
    	mov ax, stack
    	mov ss, ax
    	mov sp, 128
    	push cs
    	pop ds
    	mov ax, 0
    	mov es, ax
    	mov si, offset int9	; 设置ds:si指向源地址
    	mov di, 204h		; 设置es:di指向目的地址
    	mov cx, offset int9end-offset int9
    	rep movsb
    	push es:[9*4]	; 将原int9中断例程入口地址拷贝到[200]开始处
    	pop  es:[200h]
    	push es:[9*4+2]
    	pop  es:[202h]
    	; 安装新的int9中断例程到中断向量表
    	cli	; 关闭中断
    	mov word ptr es:[9*4], 204h	; (CS)=204h
    	mov word ptr es:[9*4+2], 0	; (IP)=0
    	sti	; 打开中断
    	mov ax, 4c00h
    	int 21h
    int9:
    	push ax
    	push bx
    	push cx
    	push es
    	int al, 60h
    	pushf
    	call dword ptr cs:[200h]	; (CS)=0,执行原有int9中断例程
    	cmp al, 3bh	; F1的扫描码
    	jne int9ret
    	mov ax, 0b800h
    	mov es, ax
    	mov bx, 1
    	mov cx, 2000
    s:	inc byte ptr es:[bx]
    	add bx, 2
    	loop s
    int9ret:
    	pop es
    	pop cx
    	pop bx
    	pop ax
    int9end:
    code ends
    end start
    

    15.6 系统指令总结

    8086CPU提供以下几大类指令:

    (1)数据传输指令

    比如:mov、push、pop、pushf、popf、xchg等,这些指令实现寄存器和内存、寄存器和寄存器之间的单个数据传送。

    (2)算术运算指令

    比如:add、sub、adc、sbb、inc、dec、cmp、imul、idiv等,这些指令实现寄存器和内存中的数据的算术运算,它们的执行结果影响标志寄存器的S-Flag、Z-Flag、O-Flag、C-Flag、P-Flag、A-Flag位。

    (3)逻辑指令

    比如:and、or、not、xor、test、shl、shr、sal、sar、rol、rcl、rcr等。除了not指令,它们的执行结果都影响标志寄存器的相关标志位。

    (4)转移指令

    可以修改IP,或同时修改CS和IP的指令统一称为转移指令,可以分为几类:

    无条件转移指令:jmp;

    条件转移指令:jcxz、je、jb、ja、jnb、jna等;

    循环指令:loop

    过程:call、ret、retf

    中断:int、iret

    (5)处理机控制指令

    这些指令对标志寄存器或其它处理及状态进行设置,比如:cld、std、cli、sti、nop、clc、cmc、stc、hlt、wait、esc、lock等。

    (6)串处理指令

    这些指令对内存中的批量数据进行处理,比如:movsb、movsw、cmps、scas、lods、stos等。若需要使用这些指令方便进行批量数据处理,需要和rep、repe、repne等前缀指令配合使用。