Orange'S:一个操作系统的实现

中断和异常机制

Posted by BINBIN Blog on November 1, 2019

中断和异常

int 15h是BIOS中断处理函数,是在实模式下调用的!在保护模式下显示出来的。

因为在保护模式下中断机制发生了很大的变化,原来的中断向量表已经被IDT 所代替,实模式下能用的BIOS 中断在保护模式下已经不能用了。

这个IDT 是新的东东,之前没有讲到。猜测是跟GDT 、 LDT 应该有相似的东西。 这个也是描述符表,叫做中断描述符表(interrupt Descript Table)。 IDT 中的描述符可以是下面三种之一:

  • 中断门描述符
  • 陷阱门描述符
  • 任务门描述符

IDT 的作用是将每一个中断向量和一个描述符对应起来。从这个意义上说,IDT也是一种向量表,虽然它形式上跟实模式下的向量表非常不同。而我们在“调用们初体验”提到中断门和陷阱门是特殊的调用门!

中断向量到中断处理程序的对应过程。

结合上文学习的调用门,中断门和陷阱门的作用几乎是一样的,只不过使用调用门使用call指令,在这里使用int指令!

IDT 中可以有中断门、陷阱门或者任务门,**但任务门在有些操作系统中根本没有用到 比如linux **。

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

可以对比任务门,把任务门的结构图也附上:

  • TSS选择子: 执行任务切换时,必须找到新任务的选择子。
  • P位:任务门的P位指示该门是否有效,p=0时,不允许使用此门实施任务切换;
  • DPL:任务门描述符的特权级,但是对因中断而发起的任务切换不起作用,处理器不按特权级施加任何保护。当以非中断的方式使用任务门进行任务切换,就需要用到DPL。

对比之后我们知道,在中断门和陷阱门中的BYTE4的低5位编程了保留位,不再是param count。而且,表示TYPE 的4位也将变位0xE(中断门)或0xF(陷阱门)。当然,S位还是0!

中断不但涉及到处理器以及指令,还涉及处理器与硬件的联系等内容。

中断和异常机制

中断和异常的概念和应用场合基本都有所了解。

基本都要有中断向量表! 作用就是关联中断处理程序!这个向量号通过IDT 就与相应的中断处理程序对应起来。

下表给出了处理器可以处理的中断和异常列表,包好它们对应的向量号以及描述!

类型中有,fault 、 Trap 、 Abort 是异常的三种类型!

  • fault故障:故障通常是可以纠正的。比如,缺页,这实际上是一种故障,只需要处理器将磁盘上对应的页拷贝到相应的页面即可。故障一般是由好处的
  • trap陷阱:陷阱通常用于调试目的。比如单步调试一般使用int3指令,这其实是一种陷阱。
  • Abort终止:终止意味着严重的错误,比如硬件错误,系统表(GDT,LDT等)中的数据不一致或者无效。一个比较典型的终止类异常是“双重故障”,一般无法修复。

异常也分为三种异常:

  • 程序错误异常:处理器再执行指令时,检测到程序的错误,并由此而引发的异常
  • 软件引发的异常:这类通常是由into、int3和bound指令主动发起的。比如中断的单步调试,就是利用int3指令进行的。
  • 机器检查异常:这种是与处理器型号相关的一些问题。我们不关心这种问题

外部中断

中断包括硬件中断和软中断。 外部中断,也就是硬件产生的中断,另外一种就是指令int n产生的中断。

使用int n 产生中断,n就是向量号,类似调用门使用。

外部中断需要建立硬件中断与向量号之间的对应关系。外部中断分为不可屏蔽中断和可屏蔽中断两种,分别由cpu的两根引脚NMI 和INTR 来接收!

硬件中断是由外围设备发出中断信号引起的,以请求处理器提供服务。硬件中断完全是随机的,与处理器的执行并不同步。当中断发生时,处理器要先执行完当前的指令,然后才对中断进行处理。

这些具体看书上所说!就不总结了! 之前的章节汇编语言也有讲解的!

代码pmtest9a.asm, 该代码目的是把设置8259A写入一个函数!

因为8259A是可编程的中断控制器,所以它的操作是用软件通过命令进行控制的。8259A的编程命令字有两类:一是初始化命令字(ICW),二是操作命令字(OCW)。相应的8259A的控制部分有一些可编程的位,它们分布在7个8位寄存器中。这些寄存器分成两组,一组用作存ICW,另一组存OCW。当计算机刚开机时,用初始化程序设定ICW,即由CPU按次序发送2~4个不同格式的ICW,用来建立起8259A操作的初始状态,此后的整个工作过程中该状态保持不变。相反操作命令字(OCW)用于动态控制中断处理,是在需要改变或控制8259A操作时发送的。注意:当发出ICW或OCW时,CPU中断申请脚INTR应关闭(使用CLI关中断指令)。

  1. ICW(ICW1、ICW2、ICW3、ICW4)初始化命令字编程格式

(1) ICW1(芯片控制初始化命令字)功能介绍:

ICW1负责启动8259A和进行初始化工作:

1) 清除IMR

2) 把最低优先权分配给IR7

3) 把最高优先权分配给IR0

4) 将从设备标志ID置成7

5) 清除特殊屏蔽方式以及设置读IRR方式

(2) ICW2(中断类型号的设置)功能介绍:

ICW2负责规定中断类型号字节。编程时规定高5位T7—T3,低3位由IR的编码写入。

例如:输入时地址线A0=1,IR0—IR7的中断向量为08H—0FH,PC/XT机中的T7—T3==00001,当IR4申请时8259向CPU发出中断申请的类型号为00001100==0CH。

(3) ICW3(主/从片初始化命令字)功能介绍:

主片ICW3:

主片ICW3负责记录与从片哪一个输入端与从片相连。

当主片输入端IRi上连接有从片的INT时,则Si=1;否则Si=0

从片ICW3:

从片ICW3负责自己连接到主片的哪一端。

应用ICW3时的注意点:

一是什么时候用ICW3:即当ICW1中的SNGI位为0时,也就是工作于级联方式,才需要ICW3设置8259A的状态。

二是(主片接出)判断哪个引脚(IR7—IR0)有级联:当D7—DO的某位为1时则接有从片,为0时不接从片。

三是(从片接入)判断接入主片的哪个引脚:是通过对D2 D1 D0三位的组合来判断接入的引脚。

ICW4负责缓冲器方式和中断结束方式的设置。

应用ICW4时的注意点:

一是什么时候写入ICW4:当ICW1的IC=1时,才使用ICW4。

二是命令字各位所代表的含义:

UPM:指定CPU类型:UPM=0时,工作于8080(8位机);UPM=1时,工作于8086(16位机)

AEOI:指定是否自动中断结束方式:1:自动中断结束方式;0:非自动中断结束方式。

BUF:8259A是否工作于缓冲方式:1:工作于缓冲方式、0:不工作于缓冲方式

SFNM:决定8259A在级联时是否工作于特殊全嵌套方式:1:工作于特殊全嵌套方式0:工作于一般全嵌套方式。

ICW (Initialization Command Word)初始化命令字。

主8259A对应的端口地址是20A和21A

从8259A对应的端口地址是A0h和A1h。

初始化过程:

1、往端口20h(主片)或A0h(从片)写入ICW1

2、往端口21h(主片)或A1h(从片)写入ICW2

3、往端口21h(主片)或A1h(从片)写入ICW3

4、往端口21h(主片)或A1h(从片)写入ICW4

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

ICW1负责启动8259A和进行初始化工作

ICW2中断类型号的设置

ICW3主从片初始化设置

ICW4方式控制设置


282	; Init8259A ---------------------------------------------------------------------------------------------
283	Init8259A:
284		mov	al, 011h
285		out	020h, al	; 主8259, ICW1.
286		call	io_delay
287
288		out	0A0h, al	; 从8259, ICW1.
289		call	io_delay
      011h转换为2进制为0001,0001

     对应的ICW1   000 PC系统必须为0,1对ICW必须为1,0 edge triggered模式,0 8字节中断向量 0 联级8259 1 需要ICW4
290
291		mov	al, 020h	; IRQ0 对应中断向量 0x20
292		out	021h, al	; 主8259, ICW2.
293		call	io_delay
294
295		mov	al, 028h	; IRQ8 对应中断向量 0x28
296		out	0A1h, al	; 从8259, ICW2.
297		call	io_delay

在往主8259A写入ICW2时,我们看到IRQ0(IRQ0在主8259A上)对应的了中断向量号20h,于是IRQ0---IRQ7就对应中断向量20h--27h。

     类似地IRQ8---IRQ15对应的中断向量28h---2Fh。对照3-11的表,我们知道20h--2Fh处于用户定义中断的范围内。
298
299		mov	al, 004h	; IR2 对应从8259
300		out	021h, al	; 主8259, ICW3.
301		call	io_delay
302
303		mov	al, 002h	; 对应主8259的 IR2
304		out	0A1h, al	; 从8259, ICW3.
305		call	io_delay

004h转换为2进制为0000,0100对应的主片ICW3,是IR2级联从片为1,其余为0,表示IR2连着从片

      002h转换为2进制为0000,0010对应的从片ICW3,可以发现 从片连的主片的IR号为010,是2,所以从片连的是主片的IR2
306
307		mov	al, 001h
308		out	021h, al	; 主8259, ICW4.
309		call	io_delay
310
311		out	0A1h, al	; 从8259, ICW4.
312		call	io_delay
313
314		mov	al, 11111110b	; 仅仅开启定时器中断
315		;mov	al, 11111111b	; 屏蔽主8259所有中断
316		out	021h, al	; 主8259, OCW1.
317		call	io_delay
318
319		mov	al, 11111111b	; 屏蔽从8259所有中断
320		out	0A1h, al	; 从8259, OCW1.
321		call	io_delay
      001h转换为2进制为0000,0001  最后的1表示是80X86模式

      ICW1-4分别放入主从8259A完毕。

      下面写的就是OCW1,OCW1是控制主从8259A中断屏蔽的。

      ICW,OCW是根据写的顺序系统自动填充的。
322
323		ret
324	; Init8259A ---------------------------------------------------------------------------------------------
325


      如果再往下写,系统就是填充OCW2,OCW2是控制EOI发送的,以通知8259A中断处理结束。

      mov   al,20h

      out    20h或A0h,al

      20h 转换为2进制为0010,0000,其对应的OCW2,正好EOI是1。

  ------------------------------------------------------------------------------------------

      延迟函数的功能是等待OUT操作的完成。

     在相应的位置添加调用Init8259A的指令后,对8259A的操作就结束了。

-------------------------------------------------------------------------------------------- 

具体分析可以看书上描述,代码中有调用延时 io_delay


351	io_delay:
352		nop
353		nop
354		nop
355		nop
356		ret

添加后8259A指令,下一步建立一个IDT

建立IDT

IDT 代码如下:


96 	; IDT
97 	[SECTION .idt]
98 	ALIGN	32
99 	[BITS	32]
100	LABEL_IDT:
101	; 门                        目标选择子,            偏移, DCount, 属性
102	%rep 255
103			Gate	SelectorCode32, SpuriousHandler, 0, DA_386IGate
104	%endrep
105
106	IdtLen		equ	$ - LABEL_IDT
107	IdtPtr		dw	IdtLen - 1	; 段界限
108			dd	0		; 基地址
109	; END of [SECTION .idt]
110

rep指令的作用是批量产生n个数据结构。本例是255个,从0开始算应为0~~ffh!

自定义中断示例: 定义了20h和80h中断


 LABEL_IDT:
      %rep 32

               Gate SelectorCode32,SpuriousHandler,0,DA_386IGate

      %endrep

      .020h: Gate SelectorCode32,ClockHandler,0,DA_386IGate

       %rep 95
               Gate SelectorCode32,SpuriousHandler,0,DA_386IGate

       %endrep

      .080h: Gate SelectorCode32,UserIntHandler,0,DA_386IGate

首先批量产生32个,16进制表示为00h~~1fh

后面接着是20h中断

后面批量产生95个,加上前面的33个,一共是128个中断,16进制表示为00h~~7fh。

所以后面接着是80h中断。

代码没啥好说的,利用了NASM的%rep 预处理指令,将每一个描述符都设置位指向 SelectorCode32:SpuriousHandler的中断门。 SpuriousHandler也很简单,在屏幕右上角打印红色的字符“!”,然后进入死循环!


358	_SpuriousHandler:
359	SpuriousHandler	equ	_SpuriousHandler - $$
360		mov	ah, 0Ch				; 0000: 黑底    1100: 红字
361		mov	al, '!'
362		mov	[gs:((80 * 0 + 75) * 2)], ax	; 屏幕第 0 行, 第 75 列。
363		jmp	$
364		iretd

加载IDT 的代码与对GDT 的处理非常类似

加载IDTR


201		; 为加载 IDTR 作准备
202		xor	eax, eax
203		mov	ax, ds
204		shl	eax, 4
205		add	eax, LABEL_IDT		; eax <- idt 基地址
206		mov	dword [IdtPtr + 2], eax	; [IdtPtr + 2] <- idt 基地址
207
208		; 加载 GDTR
209		lgdt	[GdtPtr]
210
211		; 关中断
212		cli
213
214		; 加载 IDTR
215		lidt	[IdtPtr]
216

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

中断机制就初始化完毕了,但是程序无法回到实模式!

这个参考pmtest9.asm

实现一个中断

执行int n用起来很像调用门的使用,既然我们已经熟悉了调用门,试一下int 指令!

在 [SECTION .s32] 添加代码


265		call	Init8259A
266		int	080h

由于IDT 所有的描述符都指向SelectorCode32:spuriousHandler处,所以,我们添加的代码段调用几号中断,都会在屏幕右上角打印出红色的字符。 “!”

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


96 	; IDT
97 	[SECTION .idt]
98 	ALIGN	32
99 	[BITS	32]
100	LABEL_IDT:
101	; 门                        目标选择子,            偏移, DCount, 属性
102	%rep 128
103			Gate	SelectorCode32, SpuriousHandler, 0, DA_386IGate
104	%endrep
105	.080h:		Gate	SelectorCode32, UserIntHandler,  0, DA_386IGate
106
107	IdtLen		equ	$ - LABEL_IDT
108	IdtPtr		dw	IdtLen - 1	; 段界限
109			dd	0		; 基地址

....

363	_UserIntHandler:
364	UserIntHandler	equ	_UserIntHandler - $$
365		mov	ah, 0Ch				; 0000: 黑底    1100: 红字
366		mov	al, 'I'
367		mov	[gs:((80 * 0 + 70) * 2)], ax	; 屏幕第 0 行, 第 70 列。
368		iretd
369
370	_SpuriousHandler:
371	SpuriousHandler	equ	_SpuriousHandler - $$
372		mov	ah, 0Ch				; 0000: 黑底    1100: 红字
373		mov	al, '!'
374		mov	[gs:((80 * 0 + 75) * 2)], ax	; 屏幕第 0 行, 第 75 列。
375		jmp	$
376		iretd
377

运行可以看到红色的字符”I” 在屏幕右上方!

时钟中断实验

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

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


386	; int handler ---------------------------------------------------------------
387	_ClockHandler:
388	ClockHandler	equ	_ClockHandler - $$
389		inc	byte [gs:((80 * 0 + 70) * 2)]	; 屏幕第 0 行, 第 70 列。
390		mov	al, 20h
391		out	20h, al				; 发送 EOI
392		iretd

可以看出发送EOI两行语句,以及iretd,只有一条指令!

我们在调用80h号中断之后打开中的话,由于第0行和第70列出已经被写入I,所以第一次中断发生时那里会变成字符J,再一次中断则变成K,以后每发生一次时钟中断,字符就会变化一次,就会看到不断变化的字符!

修改初始化8259A的代码,时钟中断不再屏蔽!


342		;mov	al, 11111111b	; 屏蔽主8259所有中断
343		mov	al, 11111110b	; 仅仅开启定时器中断
344		out	021h, al	; 主8259, OCW1.
345		call	io_delay
IR0控制时钟中断,所以要把最后一位设置为0,打开IR0时钟中断。

在ICW2中IR0设置的中断向量为20h,所以只要时钟中断打开,系统就会在IDT中寻找20h的中断向量,从而跳转到处理函数去执行。
346
347		mov	al, 11111111b	; 屏蔽从8259所有中断
348		out	0A1h, al	; 从8259, OCW1.
349		call	io_delay
350
351		ret

IDT修改


102	[SECTION .idt]
103	ALIGN	32
104	[BITS	32]
105	LABEL_IDT:
106	; 门                        目标选择子,            偏移, DCount, 属性
107	%rep 32
108			Gate	SelectorCode32, SpuriousHandler,      0, DA_386IGate
109	%endrep
110	.020h:		Gate	SelectorCode32,    ClockHandler,      0, DA_386IGate
111	%rep 95
112			Gate	SelectorCode32, SpuriousHandler,      0, DA_386IGate
113	%endrep
114	.080h:		Gate	SelectorCode32,  UserIntHandler,      0, DA_386IGate
115
116	IdtLen		equ	$ - LABEL_IDT
117	IdtPtr		dw	IdtLen - 1	; 段界限
118			dd	0		; 基地址
119	; END of [SECTION .idt]
120

程序马上会执行,可能没等一个中断发生程序就执行完了,所以直接让弄个死循环就算了


288		int	080h
289		sti
290		jmp	$

首先调用80h中断向量的函数

sti 打开中断,这里只打开时钟中断,所以时钟在运行时就产生中断,该中断是由IR0控制的,IR0的中断向量是20h,所以跳到20h所指向的函数去执行。

额外说明

特权级变换,上面代码一直在ring0状态下运行,在实际应用上面,中断的产生大多带有特权级变化的!

实际上通过中断门和陷阱门的中断就相当于call指令调用一个调用门,设计的特权级变换规则是完全一样的!

中断和异常发生时的堆栈变化

中断或者异常发生,以及相应的处理程序结束时,堆栈都发生变化!

如果发生中断或者异常没有特权级变换,那么eflags、cs、eip将一次被压入堆栈,如果有出错码将在最后被压栈。有特权级变化的情况下同样会发生堆栈切换,此时,ss esp将被压入内层堆栈,然后是eflags、cs、eip、出错码如果有的话。

返回必须使用指令iretd! 它同时会改变eflags的值!

注意,只有当CPL为0,eflags中的IOPL域才会改变,而且只有当CPL<= IOPL时,IF才会被改变。

另外 使用iretd执行时 error code不会被自动从堆栈弹出,所以执行之前先将它从栈中清除掉!

中断门和陷阱门的区别

这两个存在细小的差别,就是对中断允许标志IF 的影响。

由中断向量引起的中断会复位IF, 陷阱门产生的中断不会改变IF。