中断和异常
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关中断指令)。
- 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。