中断(8086)

中断就是打断处理器当前的执行流程,去执行另外一些和当前工作 不相干的指令,执行完之后,还可以返回到原来的程序流程继续执行。 这就好比是你正在用手机听歌,突然来电话了。处理器(当然,手机也 是有处理器的)必须中断歌曲的播放,来处理这件更为重要的事件。

中断的一些概念:

中断号:

由于CPU需要通过对不同类型的中断进行不同处理,所以每种类型的中断都被统一编号,这称为中断类型号、中断向量或者中断号。Intel 处理器允许256 个中断,中断号的范围是0~255

中断源:

中断信号的来源,或者说产生中断的设备,被称为中断源

中断嵌套:

当一个中断事件正在处理时,如果来了一个优先级更高的中断事件时,允许暂时中止当前的中断处理,先为优先级较高的中断事件 服务,这称为中断嵌套

实模式下的中断向量表(Interrupt Vector Table,IVT):

所谓中断处理,其实就是处理器要执行一段与该中断有关的程序(指令)你也可以将其当作一个函数。处理器可以识别256 个中断,那么理论上就需要256 段代码。这些代码实际存放的位置并不重要,重要的是,在实模式下,处理器要求将它们的入口点也就是起始地址集中存放到内存中从物理地址0x00000 开始,到0x003ff 结 束,共1KB 的空间内,这就是所谓的中断向量表。

 

 

每个中断的入口点地址在中断向量表中占2 个字,分别是中断处理代码的偏移地址和段地址。中断0的入口点位于物理地址0x00000 处, 也就是逻辑地址0x0000:0x0000;中断1 的入口点位于物理地址0x00004 处,即逻辑地址0x0000:0x0004;其他中断入口点地址以此类推。

中断分类:

中断大致上可以分为硬件中断和软件中断(简称为软中断)。

顾名思义,硬件中断由硬件来提供,比如说:CPU,鼠标键盘等。而软件键盘由内部的代码来定义。

 

 

硬件中断:

硬件中断还可以分为外部硬件中断和内部硬件中断,外部硬件中断是指除CPU以外的硬件对应的中断,而内部硬件中断是内部CPU对应的中断。

外部硬件中断:

外部硬件中断,就是除处理器以外的外部设备中来的中断信号。当外部设备发生错误,或者有数据要传送(比如,从网络中接收到一个针对当前主机的数据包),或者处理器交给它的事情处理完了(比如,打印已经完成),它们都会告诉CPU先停下工作,来临时处理一下。

外部硬件中断又可以根据中断是否紧急而细分:因为有一些严重的事情,比如说电池没电了,这个如果不处理马上就会关机了,而有一些不是很重要,比如说键盘输入,有时候卡死了为了防止外部硬件继续倒腾导致系统直接崩溃,可以选择把这种不是很重要的中断进行屏蔽掉。所以外部硬件中断又分为可屏蔽中断和不可屏蔽中断

外部硬件中断是通过两个信号线引入处理器内部的。 从8086 处理器开始,这两根线的名字就叫NMI 和INTR。

 

 

(这是一个简化的示意图, 不是真正的设备连接图。)

当一个外部硬件中断发生时,处理器将会从中断引脚NMI或INTR中得到通知,其中不可屏蔽中断采用NMI引脚,而可屏蔽中断采用INTR引脚。

不可屏蔽中断(Non Maskable Interrupt,NMI):

不可屏蔽中断是针对一些硬件的很严重的事件进行处理。所有的严重事件都必须无条件地加以处理,这种类型的中断是不会被阻断和屏蔽的,称为非屏蔽中断 (Non Maskable Interrupt,NMI)。

在传统的兼容模式下,NMI 的中断源通过一个与非门连接到处理器。处理器的NMI 引脚是高电平有效的,而中断信号是低电平有效。 当不存在中断的时候,与非门的所有输入都为高电平也就是所有中断源的都没有发出中断信息,因此与非门的结果就是低电平所以处理器的NMI 引 脚为低电平,这CPU没有从NMI引脚获得中断信息。

当至少有一个不可屏蔽中断发生时都会导致与非门的输出结果为高电平,也就会导致NMI引脚为高电平,而CPU就会接受到NMI,然后进行对应的处理。

(与非门:与非门(一种逻辑电路)_百度百科 (baidu.com)

由于不可屏蔽中断的非常特殊性,几乎所有触发NMI 的中断事件对处理器来说都是致命的,甚至是不可纠正的。所以在这种情况下,努力去搞清楚发生了什么,通常没有太大的意义,这样的事最好留到事后, 让专业维修人员来做。 因此在实模式下,NMI 被赋予了统一的中断号2,不再进行细分。一旦发生2号中断,处理器和软件系统通常会放弃继续正常工作,也不会纠正已经发生的问题和错误,很可能只是由软件系统给出一个提示信息。

可屏蔽中断(Interrupt Request)INTR:

这里的INTR就不是可屏蔽中断的简称了,而是一个Interrupt request的简称。

这类中断有两个特点,第一是数量很多,毕竟有很多外部设备;第二是它们可以被屏蔽,这样处理器就不对它们进行处理。所以,这类硬件中断称为可屏蔽中断。

可屏蔽中断是通过INTR 引脚进入处理器内部的,像NMI一样,不可能为每一个中断源都提供一个引脚。而且,处理器每次只能处理一个中断。在这种情况下,需要一个代理,来接受外部设备发出的中断信号。 还有,多个设备同时发出中断请求的几率也是很高的,所以该代理的还包括对它们抉择到底让它们中的哪一个优先向处理器提出服务请求。

在个人计算机中,用得最多的中断代理就是8259芯片,这类硬件就是通常所说的中断控制器( Interrupt Controller,IC)),从8086 处理器开始,它就一直提供这种服务。即使是现在,在绝大多数单处理器的计算机中,也依然有它的存在。

8559芯片

Intel 处理器允许256 个中断,中断号的范围是0~255,8259 提供其中的15 个,但中断号并不固定。之所以不固定,是因为设计时,允许软件根据自己的需要灵活设置中断号,以防止发生冲突。该中断控制器芯片有自己的端口号,可以像访问其他外部设备一样用in 和 out 指令来改变它的状态,包括各引脚的中断号。正是因为这样,它又叫可编程中断控制器(Programmable Interrupt Controller,PIC)

 

 

每片8259 只有8 个中断输入引脚,在个人计算机上使用它,需要两块。如图所示,第一块8259 芯片的代理输出的INT 直接送到处理器的INTR引脚,这是主片(Master);第二块 8259 芯片的INT输出送到第一块的引脚2 上,是从片(Slave),两块芯片之间形成级联(Cascade)关系。

所以两块8259 芯片可以向处理器提供15 个中断信号。根据需要,这些中断引脚可以被各种设备使用。

8259 的主片引脚0(IR0)接的是系统定时器/计数器芯片;从片的引脚0(IR0)接的是实时时钟芯片RTC这两块芯片的固定连接即使是在硬件更新换代非常频繁的今天,也没有改变。

在8259 芯片内部,有中断屏蔽寄存器(Interrupt Mask Register, IMR),这是个8 位寄存器,对应着该芯片的8 个中断输入引脚,对应的位是0 还是1,决定了从该引脚来的中断信号是否能够通过8259 送往处 理器(0 表示允许,1 表示阻断)。当外部设备通 过某个引脚送来一个中断请求信号时,如果它没有被IMR 阻断,那么, 它可以被送往处理器。

(注,8259 芯片是可编程的,主片的端口号是 0x20 和0x21,从片的端口号是0xa0 和0xa1,可以通过这些端口访问 8259 芯片,设置它的工作方式,包括IMR 的内容。)

中断能否被处理,除了要看8259 芯片的选择外,还需要由处理器来决定。在处理器内部,标志寄存器有一个标志位IF,这就是中断标志(Interrupt Flag)。当IF 为0 时,所有从处理器INTR 引脚来的中断信号都被忽略掉;当其为1 时,处 理器可以接受和响应中断。 IF 标志位可以通过两条指令cli 和sti 来改变。这两条指令都没有操作 数,cli(CLear Interrupt flag)用于清除IF 标志位,sti(SeT Interrupt flag)用于置位IF 标志为1。

在计算机内部,中断发生得非常频繁,当一个中断正在处理时,其他中断也有可能会响应,甚至会有多个中断同时发生的情况。这就需要中断控制器了,8259 芯片会记住它们,并按一定的策略决定先为谁服务。总体上来说,中断的优先级和引脚是相关的,主片的IR0引脚优先级最高,IR7引脚最低,从片也是如此。当然,还要考虑到从片是级联在主片的IR2引脚上。

小结外部硬件中断:

外部硬件中断分为可屏蔽和不可屏蔽,对应NMI和INTR两个引脚,NMI由于很严重只要发生了就直接停下就好了,而不可屏蔽中断由于功能很多需要选择所以就需要一个中断控制器来处理,中断控制器里可以选择是否响应中断,而CPU里也有IF标志位来选择是否响应中断。

内部硬件中断:

内部硬件中断发生在处理器内部,是由执行的指令引 起的。比如,当处理器检测到div 或者idiv 指令的除数为零时,或者除法的结果溢出时,将产生中断0(0 号中断),这就是除法错误中断。

内部中断不受标志寄存器IF 位的影响,它们的中断类型是固定的,可以立即转入相应的处理过程。

软中断

软中断与硬件中断无关,是写好的存放在计算机里面的中断,可以类比为一个API函数。

软中断是由int 指令引起的中断处理。中断号在指令中给出,int 指令的格式如下:

int3    //int3 是断点中断指令
int 立即数
into

 

注意,int3 和int 3是不一样的,int3是一个特殊的断点指令,而int 3是中断向量表中的第四个中断程序。

into 是溢出中断指令,机器码为0xCE,也是单字节指令。当处理器 执行这条指令时,如果标志寄存器的OF 位是1,那么,将产生4 号中 断。否则,这条指令什么也不做。

最有名的软中断是BIOS 中断,之所以称为BIOS 中断,是因为这些中断功能是在计算机加电之后,由BIOS 程序执行期间建立起来的。

CPU调用中断过程:

拿外部硬件中断来进行举例。

当中断发生时,如果从外部硬件到处理器之间的道路都是畅通的, 那么,处理器在执行完当前的指令后,会立即为硬件服务。它首先会响应中断,告诉8259 芯片(中断管理器)准备着手处理该中断。接着要求 8259 芯片把中断号送过来。8259 芯片它会把对应的中断号告诉处理器,处理器接受到中断号后就开始处理:

① 保护断点的现场。首先要将标志寄存器FLAGS 压栈,然后清除它的IF 位和TF 位。接着,再将当前的代 码段寄存器CS 和指令指针寄存器IP 压栈。

② 执行中断处理程序。由于处理器已经拿到了中断号,它将该号码乘以4(因为每个中断地址在中断向量表中占4 字节),就得到了该中断入口点在中断向量表中的偏移地址。接着,从表中依次取出中断程序的偏移地址和段地址,并分别传送到IP 和CS,然后,处理器就开始执行中断处理程序了。

(由于IF 标志被清除,在中断处理过程中,处理器将不再响应硬件中断。如果希望更高优先级的中断嵌套,可以在编写中断处理程序 时,适时用sti 指令开放中断。)

③ 返回到断点处接着执行。所有中断处理程序的最后一条指令必须是中断返回指令iret。这将导致处理器依次从栈中弹出(恢复)IP、CS 和 FLAGS 的原始内容,于是转到主程序接着执行。

;这里的iret指令用汇编来描述就是
pop ip
pop cs
popf

 

其它的中断调用过程可以参考外部硬件调用过程,只是少了一个中断管理器。

 

 

为什么进行中断处理时,需要先将IF、TF都设置为0?

  将IF设置为0的原因主要是因为8086CPU的设计者认为中断处理程序一般是不需要对其它中断做出响应的。

  因此默认的将IF设置为0,禁止CPU处理可屏蔽中断。当然,如果有的中断处理程序确实需要处理可屏蔽中断,也可以在中断处理程序中开中断,将IF重新设置为1。指令sti,设置IF=1;指令cti,设置IF=0。

  将TF单步调试功能关闭的原因则是为了避免出现单步中断的死循环。

  试想如果开启了单步调试功能,那么在进入中断处理程序,并执行完第一条指令后,便会引发单步中断,进而跳转到单步中断处理程序中。而在执行了单步中断处理程序的第一条指令后,又会再度引发单步中断,这成为了一个死循环。因此,在进入中断处理程序之前,必须首先把TF置为0,关闭单步调试功能,以避免上述情况产生。

 

 

手动编写软中断:

首先明白如何写中断。

1:编写中断程序

2:将中断程序保存在内存中的某一个位置

3:将中断程序的入口点保存到中断向量表里

4:调用中断程序。

1 编写中断程序:

这里我采用了很简单的代码,修改显卡的第一个字符的内容和颜色:

show:
    mov ax,0xb800
    mov es,ax
    mov byte es:[0],0x48
    mov byte es:[1],0x04
    iret

 

2 将中断程序保存在内存中:

8086CPU的内存分布:

 

 

我们可以将我们的中断程序保存到DRAM内存条里,我们需要找一块空的内存地址就可以。

这里我查看了0x10000物理地址往上的地址空间:

 

 

 

 

发现是空的,我就采用这块地址空间了。

然后我们需要把中断程序的硬编码保存进去。

    mov ax,0
    mov ds,ax
    mov ax,0x1000
    mov es,ax
    mov si,show
    mov di,0
    mov cx,showend-show
    cld
    rep movsb

 

3 将中断程序的入口点保存到中断向量表里

首先用bochs虚拟机查看中断向量表的空余中断地址:

 

 

我选择了这里物理地址为0x180的地方,然后我们将入口点的段地址和偏移地址赋值进去。

    mov ax,0x0
    mov es,ax
    mov es:[0x180],word 0x0
    mov es:[0x182],word 0x1000
    ;//先是偏移地址,再是段地址

 

4 调用中断程序

调用软中断很简单,只需要int n就行了,n是你的中断地址相对于起始中断地址的一个数组偏移值。

这里 n = 行数-1 * 4+对于行的起始地址的偏移量 =24*4+0=96

所以就直接调用指令就可以了:

    int 96

 

验证中断调用是否成功:

整个汇编程序的源代码:

segment myInterrupt vstart=0x7c00
​
​
;将机器码赋值给0x1000:0的内存地址空间
start:
    mov ax,0
    mov ds,ax
    mov ax,0x1000
    mov es,ax
    mov si,show
    mov di,0
    mov cx,showend-show
    cld
    rep movsb;将中断入口点地址保存到中断向量表
    mov ax,0x0
    mov es,ax
    mov es:[0x180],word 0x0
    mov es:[0x182],word 0x1000
    
    int 96
    
    jmp start
​
​
show:
    mov ax,0xb800
    mov es,ax
    mov byte es:[0],0x48
    mov byte es:[1],0x04
    iretshowend:
    nop
​
times 510-($-$$) db 0
                 db 0x55,0xaa

 

将其编译后写入硬盘的主引导扇区就可以加载了。

采用bochs 调试虚拟机来调试,首先将程序运行到主引导扇区的地方:

 

 

然后查看我们的汇编指令,并给int 96指令打一个断点:

 

 

然后运行到断点处并查看内存是否写入:

 

 

可以看到已经成功写入内存地址为0x10000的地址空间了。

然后查看中断向量表是否成功写入:

 

 

可以看到内存地址 0x180的内容已经更改为 0x10000000,因为intel是采用的小端字节序,所以我们肯定更改成功了。

然后我们给int 96中断处理程序的入口地址打一个断点,并运行查看是否允运行到了我们的中断处理程序:

 

 

可以看到已经运行到我们的中断处理程序处了,而且此时的物理地址也改为了0x10000(可能有人想问前面的if不是置零了吗,int3断点中断也是中断啊,为啥这里就可以中断嵌套呢,其实很简单,那个if是针对外部中断设备的可屏蔽中断的,我们这个是软中断,和它无关。)

然后我们将这个中断处理程序运行完,并查看显示器的内容是否修改:

 

 

可以看到完美实现。所以我们的代码逻辑肯定没有问题,以上就是一个简单的软中断实现。

小结:

中断是CPU的一个重要概念,需要掌握。当然,这次是讲述的8086中断,对于其它CPU虽然不吻合,但是具有很大的参考意义。