• 汇编-外中断


    我们知道, CPU在计算机系统中, 除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出。也就是说, CPU除了有运算能力外, 还要有I/O(Input/Output, 输入/输出) 能力。比如, 我们按下键盘上的一个键, CPU最终要能够处理这个键。在使用文本编辑器时, 按下a键后, 我们可以看到屏幕上出现“a”, 是CPU将从键盘上输入的键所对应的字符送到显示器上的。

    要及时处理外设的输入, 显然需要解决两个问题:①外设的输入随时可能发生, CPU 如何得知?②CPU从何处得到外设的输入?
    这一章中,我们以键盘输入为例,讨论这两个问题。


    接口芯片和端口

    PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器, CPU将这些寄存器当作端口来访问。
    外设的输入不直接送入内存和CPU, 而是送入相关的接口芯片的端口中; CPU向外设的输出也不是直接送入外设, 而是先送入端口中, 再由相关的芯片送到外设。CPU还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。
    可见, CPU通过端口和外部设备进行联系。
     

    外中断信息
     

    现在,我们知道了外设的输入被存放在端口中,可是外设的输入随时都有可能到达,CPU如何及时地知道, 并进行处理呢?更一般地讲, 就是外设随时都可能发生需要CPU 及时处理的事件, CPU如何及时得知并进行处理?.
    CPU提供中断机制来满足这种需要。前面讲过,当CPU的内部有需要处理的事情发生的时候, 将产生中断信息, 引发中断过程。这种中断信息来自CPU的内部。
    还有一种中断信息, 来自于CPU外部, 当CPU外部有需要处理的事情发生的时候,比如说, 外设的输入到达, 相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。
    在PC系统中,外中断源一共有以下两类


    1.可屏蔽中断
     

    可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断, 要看标志寄存器的IF位的设置。当CPU检测到可屏蔽中断信息时, 如果IF=1, 则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断
    我们回忆一下内中断所引发的中断过程:

    (1)取中断类型码n;
    (2)标志寄存器入栈,IF=0,TF=0;

    (3)CS、IP入栈;
    (4)(IP)=(n*4),(CS)=(n*4+2)

    由此转去执行中断处理程序。
    可屏蔽中断所引发的中断过程,除在第1步的实现上有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自于CPU外部, 中断类型码是通过数据总线送入CPU的; 而内中断的中断类型码是在CPU内部产生的。
    现在,我们可以解释中断过程中将IF置为0的原因了。将IF置0的原因就是,在进入中断处理程序后,禁止其他的可屏蔽中断。
    当然, 如果在中断处理程序中需要处理可屏蔽中断, 可以用指令将IF置1。8086CPU 提供的设置IF的指令如下:

    sti, 设置IF=1;
    cli,设置IF=0。
     

    2.不可屏蔽中断
     

    不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时, 则在执行完当前指令后,立即响应,引发中断过程。
    对于8086CPU, 不可屏蔽中断的中断类型码固定为2, 所以中断过程中, 不需要取中断类型码。则不可屏蔽中断的中断过程为:
    (1)标志寄存器入栈,IF=0,TF=0;
    (2)CS、IP入栈;
    (3)(IP)=(8),(CS)=(0AH)。
    几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如说
    键盘输入) 发生时, 相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。在我们的课程中, 主要讨论可屏蔽中断
     

    PC机键盘的处理过程
     

    下面我们看一下键盘输入的处理过程,并以此来体会一下PC机处理外设输入的基本方法。


    1.键盘输入


    键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。
    按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h
    松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入60h端口中。
    一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。扫描码长度为一个字节,通码的第7位为0,断码的第7位为1,即:
    断码=通码+80h
    比如,g键的通码为22h,断码为a2h。

    2.引发9号中断
     

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

    3.执行int 9中断例程
     

    BIOS提供了int 9中断例程, 用来进行基本的键盘输入处理, 主要的工作如下:
    (1)读出60h端口中的扫描码;
    (2)如果是字符键的扫描码, 将该扫描码和它所对应的字符码(即ASCII码) 送入内存中的BIOS键盘缓冲区; 如果是控制键(比如Ctrl) 和切换键(比如CapsLock) 的扫描码, 则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元;
    (3)对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
    BIOS键盘缓冲区是系统启动后, BIOS用于存放int 9中断例程所接收的键盘输入的内存区。该内存区可以存储15个键盘输入, 因为int 9中断例程除了接收扫描码外, 还要产生和扫描码对应的字符码, 所以在BIOS键盘缓冲区中, 一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
    0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下。


    0:右shift状态, 置1表示按下右shift键;

    1:左shift状态, 置1表示按下左shift键;

    2:Ctrl状态, 置1表示按下Ctrl键;

    3:Alt状态, 置1表示按下Alt键;

    4:Scroll Lock状态, 置1表示Scroll指示灯亮;

    5:NumLock状态, 置1表示小键盘输入的是数字;

    6:CapsLock状态, 置1表示输入大写字母;

    7:Insert状态, 置1表示处于删除态;
     

    编写int 9中断例程
     

    从上面的内容中,可以看出键盘输入的处理过程:①键盘产生扫描码;②扫描码送入60h端口; ③引发9号中断; ④CPU执行int 9中断例程处理键盘输入。
    上面的过程中, 第1、2、3步都是由硬件系统完成的。我们能够改变的只有int 9中断处理程序。我们可以重新编写int 9中断例程, 按照自己的意图来处理键盘的输入。但是,在课程中,我们不准备完整地编写一个键盘中断的处理程序,因为要涉及一些硬件细节,而这些内容脱离了我们的内容主线。
    但是,我们却还要编写新的键盘中断处理程序,来进行一些特殊的工作,那么这些硬件细节如何处理呢?这点比较简单, 因为BIOS提供的int 9中断例程已经对这些硬件细节进行了处理。我们只要在自己编写的中断例程中调用BIOS的int 9中断例程就可以了。
    编程:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下Esc键后, 改变显示的颜色。
    我们先来看一下如何依次显示a---z

    1. assume cs:code
    2. code segment
    3. start:
    4. mov ax,0b800h
    5. mov es,ax
    6. mov ah, 'a'
    7. s:mov es:[160*12+40*2],ah
    8. inc ah
    9. cmp ah,'z'
    10. jna s
    11. mov ax,4c00h
    12. int 21h
    13. code ends
    14. end start

    在上面的程序的执行过程中,我们无法看清屏幕上的显示。因为一个字母刚显示到屏幕上, CPU执行几条指令后, 就又变成了另一个字母, 字母之间切换得太快, 无法看清。
    应该在每显示一个字母后,延时一段时间,让人看清后,再显示下一个字母。那么如何延时呢?我们让CPU执行一段时间的空循环。因为现在CPU的速度都非常快, 所以循环的次数一定要大,用两个16位寄存器来存放32位的循环次数。如下:
     

    1. assume cs:code
    2. code segment
    3. start:
    4. mov ax,0b800h
    5. mov es,ax
    6. mov ah, 'a'
    7. s:mov es:[160*12+40*2],ah
    8. call delay
    9. inc ah
    10. cmp ah,'z'
    11. jna s
    12. mov ax,4c00h
    13. int 21h
    14. delay:push ax ;延时子程序
    15. push dx
    16. mov dx,3h ;循环次数,读者可以根据自己机器的速度调整循环次数
    17. mov ax,0
    18. sl:sub ax,1
    19. sbb dx,0
    20. cmp ax,0
    21. jne sl
    22. cmp dx,0
    23. jne sl
    24. pop dx
    25. pop ax
    26. ret
    27. code ends
    28. end start

    显示“a”~“z”, 并可以让人看清, 这个任务已经实现。那么如何实现, 按下Esc键后,改变显示的颜色呢?
    键盘输入到达60h端口后, 就会引发9号中断, CPU则转去执行int 9中断例程。我们可以编写int 9中断例程, 功能如下。
    (1)从60h端口读出键盘的输入;

    (2)调用BIOS的int 9中断例程, 处理其他硬件细节;

    (3) 判断是否为Esc的扫描码, 如果是, 改变显示的颜色后返回; 如果不是则直接返回。
    下面对这些功能的实现一一进行分析。
     

    1.从端口60h读出键盘的输入

    in al,60h

    2.调用BIOS的int 9中断例程


    有一点要注意的是, 我们写的中断处理程序要成为新的int 9中断例程, 主程序必须要将中断向量表中的int 9中断例程的入口地址改为我们写的中断处理程序的入口地址。则在新的中断处理程序中调用原来的int 9中断例程时, 中断向量表中的int 9中断例程的入口地址却不是原来的int 9中断例程的地址。所以不能使用int指令直接调用。
    要能在我们写的新中断例程中调用原来的中断例程,就必须在将中断向量表中的中断例程的入口地址改为新地址之前,将原来的入口地址保存起来。这样,在需要调用的时候,我们才能找到原来的中断例程的入口。
    对于我们现在的问题, 假设将原来int 9中断例程的偏移地址和段地址保存在ds:[0] 和ds:[2] 单元中。那么我们在需要调用原来的int 9中断例程时候, 就可以在ds:[0] 、ds:[2] 单元中找到它的入口地址。
    那么,有了入口地址后,如何进行调用呢?
    当然不能使用指令int 9来调用。我们可以用别的指令来对int指令进行一些模拟, 从而实现对中断例程的调用。
    我们来看, int指令在执行的时候, CPU进行下面的工作。

    (1)取中断类型码n;

    (2)标志寄存器入栈;

    (3)IF=0,TF=0;

    (4)CS、IP入栈;

    (5)(IP)=(n*4),(CS)=(n*4+2)。
    取中断类型码是为了定位中断例程的入口地址,在我们的问题中,中断例程的入口地址已经知道。所以, 我们用别的指令模拟int指令时候, 不需要做第(1) 步。在假设要调用的中断例程的入口地址在ds:0和ds:2单元中的前提下, 我们将int过程用下面几步模拟。
    (1)标志寄存器入栈;

    (2)IF=0,TF=0;

    (3)CS、IP入栈;

    (4)(IP)=(ds:0),(CS)=(ds:2)。

    可以注意到第(3) 、(4) 步和call dword ptr ds:[0] 的功能一样, call dword ptr ds:[0] 的功能也是:
    (1)CS、IP入栈;
    (2)(IP)=((ds)*16+0),(CS)=((ds)*16+2)。
    所以int过程的模拟过程变为:

    (1)标志寄存器入栈;

    (2)IF=0,TF=0;

    (3)call dword ptr ds:[0] 
     

    对于(1) , 可用pushf实现;

    对于(2),可用下面的指令实现:

    pushf     ;标志寄存器入栈

    pop ax    ;ax=标志寄存器数据

    and ah, 11111100b   ;IF和TF为标志寄存器的第9位和第8位

    push ax

    popf

    则模拟int指令的调用功能, 调用入口地址在ds:0、ds:2中的中断例程的程序为:

    pushf     ;标志寄存器入栈

    ;TF=0,IF=0

    pushf

    pop ax

    and ah, 11111100b

    push ax

    popf

    ;

    call dword ptr ds:[0];CS、IP入栈;(IP)=((ds)*16+0),(CS)=((ds)*16+2)

    3.如果是Esc的扫描码, 改变显示的颜色后返回


    如何改变显示的颜色?
    显示的位置是屏幕的中间,即第12行40列,显存中的偏移地址为:160*12+40*2。
    所以字符的ASCII码要送入段地址b800h, 偏移地址160*12+40*2处。而段地址b800h,偏移地址160*12+40*2+1处是字符的属性,只要改变此处的数据就可以改变在段地址b800h,偏移地址160*12+40*2处显示的字符的颜色了。

    该程序的最后一个问题是, 要在程序返回前, 将中断向量表中的int 9中断例程的入口地址恢复为原来的地址。否则程序返回后,别的程序将无法使用键盘。
    经过分析,完整的程序如下。
     

    1. assume cs:code
    2. stack segment ;栈
    3. db 128 dup(0)
    4. stack ends
    5. data segment
    6. dw 0,0
    7. data ends
    8. code segment
    9. start:
    10. mov ax,stack
    11. mov ss,ax
    12. mov sp,128
    13. mov ax,data
    14. mov ds,ax
    15. mov ax,0
    16. mov es,ax
    17. push es:[9*4] ;int 9中断向量表中的入口地址的IP
    18. pop ds:[0] ;保存int 9中断向量表中的入口地址的IP
    19. push es:[9*4+2] ;int 9中断向量表中的入口地址的CS
    20. pop ds:[2] ;保存int 9中断向量表中的入口地址的CS
    21. mov word ptr es:[9*4],offset int9 ;修改中断向量表中的IP
    22. mov es:[9*4+2],cs ;修改中断向量表中的CS
    23. mov ax,0b800h
    24. mov es,ax
    25. mov ah, 'a'
    26. s:mov es:[160*12+40*2],ah
    27. call delay
    28. inc ah
    29. cmp ah,'z'
    30. jna s
    31. mov ax,4c00h
    32. int 21h
    33. delay:push ax ;延时子程序
    34. push dx
    35. mov dx,3h ;循环10000000h次,读者可以根据自己机器的速度调整循环次数
    36. mov ax,0
    37. sl:sub ax,1
    38. sbb dx,0
    39. cmp ax,0
    40. jne sl
    41. cmp dx,0
    42. jne sl
    43. pop dx
    44. pop ax
    45. ret
    46. ;新的int9中断例程
    47. int9:push ax
    48. push bx
    49. push es
    50. in al,60h ;从60端口读取数据
    51. pushf ;保存标志寄存器
    52. ;TF=0 IF=0
    53. pushf
    54. pop bx
    55. and bh,11111100b
    56. push bx
    57. popf
    58. call dword ptr ds:[0] ;对int指令进行模拟, 调用原来的int 9中断例程
    59. cmp al,1 ;ESC键的扫描码是1
    60. jne int9ret ;不等于则转移
    61. mov ax,0b800h
    62. mov es,ax
    63. inc byte ptr es:[160*12+40*2+1] ;将属性值加1,改变颜色
    64. int9ret:pop es
    65. pop bx
    66. pop ax
    67. iret
    68. code ends
    69. end start

    安装新的int 9中断例程
     

    下面, 我们安装一个新的int 9中断例程, 使得原int 9中断例程的功能得到扩展。任务:安装一个新的int 9中断例程。
    功能:在DOS下, 按F1键后改变当前屏幕的显示颜色, 其他的键照常处理。

    我们进行一下分析。

    (1)改变屏幕的显示颜色
     

    改变从B800H开始的4000个字节中的所有奇地址单元中的内容,当前屏幕的显示颜色即发生改变。程序如下:
    mov ax,0b800h

    mov es,ax

    mov bx,1  

    mov cx,2000

    S:inc byte ptr es:[bx]

    add bx,2

    loop s
     

    (2)其他键照常处理

    可以调用原int 9中断处理程序, 来处理其他的键盘输入。

    (3)原int 9中断例程入口地址的保存

    因为在编写的新int 9中断例程中要调用原int 9中断例程, 所以, 要保存原int 9中断例程的入口地址。保存在哪里?显然不能保存在安装程序中,因为安装程序返回后地址将丢失。我们将地址保存在0:200单元处。

    (4)新int 9中断例程的安装

    这个问题在前面已经详细讨论过。我们可将新的int 9中断例程安装在0:204处。
    完整的程序如下。



     

  • 相关阅读:
    element table表格实现多列排序并清除排序
    beego框架 golang web框架-网上花店
    高斯滤波算法及例程
    debian apt安装mysqlodbc
    MySQL存储引擎
    8、SpringBoot_多环境开发
    我国数据泄露事件超5100万起,全球排名第三
    单例模式读取配置文件
    【4天快速入门Python数据挖掘之第1天】Matplotlib的使用
    如何实现24/7客户服务自动化?
  • 原文地址:https://blog.csdn.net/lm68140318/article/details/132766812