您的位置:首页 > 其它

一个操作系统的实现(9)-中断和异常

2016-06-09 14:06 357 查看
这节讲了中断与异常的一些基本概念。然后通过代码实现一个显示字符的中断和时钟中断。


实模式与保护模式下的中断有区别

保护模式下的中断与实模式下的中断有几点不同。
实模式下的中断向量表在保护模式下被IDT取代
实模式下可以使用BIOS中断,而保护模式下不能用

这里面出现了一个新的名词IDT,接下来就介绍什么是IDT。


中断描述符表(IDT,Interrupt
Descriptor Table)


中断描述符表的作用

与GDT和LDT一样,IDT也是一个描述符表,IDT的描述符可以是下面三种之一:
中断门描述符
陷阱门描述符
任务门描述符

IDT作用可以用下面的图来描述:





上图是指令int n产生中断时的情形。n即为向量号,它类似于调用门的使用。

从上图可以看出,IDT的作用是将每一个中断向量和一个中断描述符对应起来。虽然形式上跟实模式下的向量表非常不同,但是从某种意义上来说,IDT也是一种向量表。

接下来看看上面的那几个门。中断门和陷阱门是特殊的调用门。他们的作用机理基本相同。只是调用门使用call指令,而中断门和陷阱门使用的是int指令。任务门这里不做描述,并不是每个操作系统都有任务门,Linux系统就没有。

接下来看看IDT中的中断门与陷阱门的结构:


IDT中的中断门与陷阱门的结构

中断门和陷阱门的结构如下:





保留位:与调用们相比,中断门和陷阱门结构中的BYTE4中的低5位为保留位,而不是调用门中的Param Count

S 位:指明描述符是数据段/代码段描述符(S=1)还是系统段/门描述符(S=0)。这里S=0

P 位 存在(Present)位。P=1表示段在内存中存在;P=0表示段在内存中不存在。

D P L 描述符特权级(Descriptor Privilege Level)。特权级可以是0、1、2或者3。数字越小特权级越大。

TYPE的4位:为0xE(中断门)或0xF(陷阱门)。

其他的一些书上3.1.4有详细介绍。接下来介绍中断与异常。


中断与异常机制

中断与异常通常在一起讨论。实际上,他们都是程序执行过程中的强制性转移,转移到相应的处理程序。

中断通常在程序执行时因为硬件而随机发生,它们通常是用来处理处理器外部的事件,如外围设备的请求。软件通过执行int n指令也可以产生中断。

再来看看异常,异常通常是在处理器执行指令时检查到错误时发生,比如遇到零除的情况。处理器检测的错误条件有很多,比如保护违例,页错误等。

不管中断还是异常,通俗来讲,都是软件或者硬件发生了某种情形而通知处理器的行为。于是,由此引出两个问题:一是处理器可以对何种类型的通知做出反应;二是当接到某种通知时做出何种处理。

第二个问题是通过中断向量解决的,中断向量对应着中断处理程序。

对于第一个问题,处理器能处理的中断和异常如下图所示:





Fault、Trap和Abort是异常的三种类型,它们的具体解释如下:
Fault是一种可被更正的异常,而且一旦被更正,程序可以不失连续性地继续执行。当一个fault发生时,处理器会把产生fault的指令之前的状态保存起来。异常处理程序的返回地址将会是产成fault的指令,而不是其后的那条指令。
Trap是一种在发生trap的指令执行之后立即被报告的异常,它也允许程序或任务不失连续性地继续执行。异常处理程序的返回地址将会是产成trap的指令之后的那条指令。
Abort是一种不总是报告精确异常发生位置的异常,它不允许程序或任务继续执行,而是用来报告严重错误的。

上面说的是异常,接下来看看中断:

中断产生的原因有两种,一种是外部中断,也就是由硬件产生的中断,另一种是由指令int n产生的中断。

指令int n产生中断时的情形上面已经说过了。下面来看外部中断。

外部中断需要建立硬件中断与向量号之间的对应关系。外部中断分为不可屏蔽中断(NMI)和可屏蔽中断两种,分别由CPU的两根引脚NMI和INTR来接收。如下图所示:





NMI不可屏蔽,因为它与IF是否被设置无关。NMI中断对应的中断向量号为2,这在上面处理器能处理的中断和异常的表中已经有所说明。

可屏蔽中断与CPU的关系是通过对可编程中断控制器8259A建立起来的。你可以认为它是中断机制中所有外围设备的一个代理,这个代理不但可以根据优先级在同时发生中断的设备中选择应该处理的请求,而且可以通过对其寄存器的设置来屏蔽或打开相应的中断。

由上图我们知道,与CPU相连的是两片级联的8259A,每个8259A有8根中断信号线,于是两片级联总共可以挂接15个不同的外部设备。那么,这些设备发出的中断请求如何与中断向量对应起来呢?就是通过对8259A的设置完成的。在BIOS初始化它的时候,IRQ0~IRQ7被设置为对应向量号08h~0Fh,而通过表我们知道,在保护模式下向量号08h-0Fh已经被占用了,所以我们不得不重新设置主从8259A。

还好,8259A是可编程中断控制器,对它的设置并不复杂,是通过向相应的端口写入特定的ICW(Initialization Command Word)来实现的。主8259A对应的端口地址是20h和21h,从8259A对应的端口地址是A0h和A1h。ICW共有4个,每一个都是具有特定格式的字节。为了先对初始化8259A的过程有一个概括的了解,我们过一会儿再来关注每一个ICW的格式,现在,先来看一下初始化过程:
往端口20h(主片)或A0h(从片)写入ICW1。
往端口21h(主片)或A1h(从片)写入ICW2。
往端口21h(主片)或A1h(从片)写入ICW3。
往端口21h(主片)或A1h(从片)写入ICW4。

这四步的顺序是不能颠倒的。

接下来看看ICW的格式:





能够看到在写入ICW2涉及与中断向量号的对应,这就是问题的所在了。

所以接下来就来开启中断的实验。所做的工作主要就是两个:设置8259A和建立IDT。


开启中断的实验


编程操作8259A



这段代码分别往主、从两个8259A各写入了4个ICW。在往主8259A写入ICW2时,我们看到IRQ0对应了中断向量号20h,于是,IRQ0~IRQ7就对应中断向量20h~27h;类似地,IRQ8~IRQ15对应中断向量28h~2Fh。20h~2Fh处于用户定义中断的范围内。

在这段代码的后半部分,我们通过对端口21h和A1h的操作屏蔽了所有的外部中断,这一次写入的不再是ICW了,而是OCW(Operation Control Word)。OCW共有3个,OCW1、OCW2和OCW3。由于我们只在两种情况下用到它,因此并不需要了解所有的内容。这两种情况是:
屏蔽或打开外部中断。
发送EOI给8259A以通知它中断处理结束。

若想屏蔽或打开外部中断,只需要往8259A写入OCW1就可以了,OCW1的格式如下:





可见,若想屏蔽某一个中断,将对应那一位设成1就可以了。实际上,OCW1是被写入了中断屏蔽寄存器(IMR,全称Interrupt Mask Register)中,当一个中断到达,IMR会判断此中断是否应被丢弃。

对于EOI。当每一次中断处理结束,需要发送一个EOI给8259A,以便继续接收中断。而发送EOI是通过往端口20h或A0h写OCW2来实现的。OCW2的格式如下图所示。





发送EOI给8295A可由如下代码完成:


对于EOI的其他各位,暂时不关注。

对于初始化8259A的代码中。每一次I/O操作之后都调用了一个延迟函数io_delay以等待操作的完成。函数io_delay很简单,调用了4个nop指令


在相应的位置添加调用Init8259A的指令之后,对8259A的操作就结束了,我们下面就来建立一个IDT。


建立IDT

为了操作方便,我们把IDT放进一个单独的段中


看得出,这个IDT真的是不能再简单了,全部的255个描述符完全相同。这里利用了NASM的%rep预处理指令,将每一个描述符都设置为指向SelectorCode32:SpuriousHandler的中断门。SpuriousHandler也很简单,在屏幕的右上角打印红色的字符“!”,然后进入死循环。代码如下:


接下来加载IDT,与加载GDT的代码很相似:


在执行lidt之前用cli指令清IF位,暂时不响应可屏蔽中断。

其实,到这里为止,我们的中断机制已经初始化完毕了,不过此时运行的话,你会发现程序无法正常回到实模式。因为IDTR以及8259A等内容已经被我们改变,要想顺利跳回实模式还要将它们恢复原样才行。

不过,即便添加了回到实模式的代码,我们仍然看不出任何效果。虽然我们已经完成了保护模式下中断异常处理机制的初始化,但并没有利用中断来做任何事。下面就继续修改代码。


实现一个中断

接下来通过int n实现一个中断,代码如下:


由于IDT中所有的描述符都指向SelectorCode32:SpuriousHandler处。SelectorCode32:SpuriousHandler处的代码如下:


所以,无论我们添加的代码调用几号中断,都应该在屏幕的右上角打印出红色的字符。运行一下,你会看到,屏幕右上角出现红色的“!”,并且程序进入死循环。结果图如下:





接下来,让程序变得优雅一些:

修改一下IDT,把第80h号中断单独列出来,并新增加一个函数来处理这个断:UserIntHandler。UserIntHandler与SpuriousHandler类似,只是在函数末尾通过iretd指令返回,而不是进入死循环。代码如下:


结果图如下:





因为在代码268行有
jmp $
,所以现在还是进入死循环,但这次的死循环与上面的有所不同,这次已经从中断处理函数中返回了。

上面的过程实现了一个中断。使用起来欲调用门差不多。而且没有用到8259A。下面来一点新鲜的体验:打开时钟中断(IRQ0)。


时钟中断实验

我们提到过,可屏蔽中断与NMI的区别在于是否受到IF位的影响,而8259A的中断屏蔽寄存器(IMR)也影响着中断是否会被响应。所以,外部可屏蔽中断的发生就受到两个因素的影响,只有当IF位为1,并且IMR相应位为0时才会发生。

那么,如果我们想打开时钟中断的话,一方面不仅要设计一个中断处理程序,另一方面还要设置IMR,并且设置IF位。设置IMR可以通过写OCW2来完成,而设置IF可以通过指令sti来完成。

接下来写一个最简单的时钟中断处理程序


这段代码的功能很简单:把屏幕第0行第70列的字符增一,变成ASCII码表中位于它后面的字符。然后发送EOI并iretd返回。如果我们调用80h号中断之后打开中断的话,由于第0行第70列已经被写入字符
I
,所以第一次中断发生时哪里会变成字符
J
,再一次中断则变成
K
。每次中断,字符就会变化一次,所以能够看到不断变化的字符。

时钟中断处理函数已经写好了。接下来修改初始化8259A的代码,使时钟中断不再被屏蔽。代码如下:


然后把IDT修改成如下的样子:


按理说,现在在调用80h号中断之后执行sti来打开中断,效果就应该可以看到了。可是有一个问题:程序马上会继续执行,可能没等第一个中断发生程序已经执行完并退出了。所以,我们需要让程序停留在某个地方,干脆让它死循环吧,这样虽然不雅,却简单易行:


接下来运行,查看效果,能够发现右上角字符在跳动。这儿截图某一个瞬间如下:





接下来是程序的完整源代码


源代码


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息