本文将介绍中断的基础知识,并通过一些示例感受一些中断。

中断

中断就是打断CPU当前的执行流程,让CPU去处理一下别的事情。当然,CPU也可以选择拒绝。

中断的分类

中断按中断源可以分为内部中断外部中断

内部中断

内部中断可以由中断指令int来触发,也可以是因为指令执行中出现了错误而触发,例如运算结果溢出会触发溢出中断;除法指令的除数为0会触发除法出错中断。

外部中断

外部中断通过NMIINTR这两条中断信号线接入CPU

  • NMI接入的是非屏蔽中断(Non Maskable Interrupt),来自这个引脚的中断请求信号是不受中断允许标志IF限制的,CPU接收到非屏蔽中断请求后,无论当前正在做什么事情,都必须在执行完当前指令后响应中断。因此非屏蔽中断常用于系统掉电处理,紧急停机等重大故障时。NMI统一被赋予中断号2

  • INTR接入的是可屏蔽中断。在IBM PC/AT机中,这个信号由两片8259A级联组成,接入CPU的中断控制逻辑电路,可管理15级中断。

中断向量表

8086的中断系统可以识别256个不同类型的中断,每个中断对应一个0~255的编号,这个编号即中断类型码。每个中断类型码对应一个中断服务程序的入口地址,256个中断,理论上就需要256段中断处理程序。在实模式下,处理器要求将它们的入口点集中存放到内存中从物理地址 0x00000开始,到0x003ff结束,共1KB的空间内,这就是所谓的中断向量表(Interrupt Vector Table, IVT)

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

中断处理过程

  1. 保护断点的现场。先将标志寄存器FLAGS压栈,然后清除IF位和TF位。将当前的代码段寄存器cs和指令指针寄存器ip压栈。

  2. 执行中断处理程序。将中断类型码乘以4(每个中断在中断向量表中占4个字节),得到了该中断入口点在中断向量表中的偏移地址。从中断向量表中依次取出中断程序的偏移地址和段地址,分别替换ipcs以转入中断处理程序执行。

  3. 返回到断点接着执行。中断处理程序的最后一条指令必须是中断返回指令iretiret执行时处理器依次从堆栈中弹出ip、cs、flags,于是处理器转到主程序继续执行。

下面我们通过几个例子感受一下。

实战

示例一

该示例演示内部中断。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.code16

# 设置了两个符号常量,类似于c语言中的#define。
.set	INIT_TYPE_CODE, 0x70		# 表示我们要使用的中断类型码,这个示例中我们打算手动触发0x70号中
.set	INIT_HANDLER_BASE, 0x07c0	# 表示中断处理程序所在的段地址。

	movw	$0xb800, %ax
	movw	%ax, %es
	
	movw	$0x7c00, %sp
	
	call	install_ivt		# 调用安装中断向量表的例程
	
	int	$INIT_TYPE_CODE		# 手动触发中断
	
	jmp	.

install_ivt:
	movw	$INIT_TYPE_CODE, %bx
	shlw	$2, %bx			# 根据中断号计算中断向量在中断向量表中的偏移地址。计算方法是左移2位,即乘4

	movw	$handler, (%bx)		# 将中断处理程序的段内偏移写入中断向量对应的偏移地址的前两个字节
	movw	$INIT_HANDLER_BASE, 2(%bx)	#  将中断处理程序所在的段地址写入中断向量对应的偏移地址的后两个字节。
	ret


# 断处理程序。只打印了一个字符,然后通过iret返回。
handler:
	movw	$'A' | 0x0a00, %es:0
	iret

	.org	510
	.word	0xAA55

运行

1
2
3
$ as --32 boot.s -o boot.o
$ objcopy -O binary -j .text boot.o boot.bin
$ qemu-system-i386 boot.bin

示例二

该示例演示外部中断。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.code16

# 设置了两个符号常量,类似于c语言中的#define。
.set	INIT_TYPE_CODE, 0x08		# 0x08。这个中断号在BIOS对8259a做过初始化之后是分配给主片的0级中断的,这个引脚用于连接8254可编程定时/计数器。
					# 8254在被BIOS初始化后会每隔54.925 ms向这个引脚输出1个信号。

.set	INIT_HANDLER_BASE, 0x07c0	# 表示中断处理程序所在的段地址。

.set	_8259A_MASTER, 0x20		# 8259a的主片0x20端口。分配给8259a主片的端口是0x20、0x21,从片的端口是0xa0, 0xa1。
					# 这个示例中我们不对8259a进行编程,但是在中断处理完成之后需要通过0x20告诉主片这个中断已经处理完了。
					# 如果中断来自从片的话那就需要同时向主片,从片发送处理完成的信号。

	movw	$0xb800, %ax
	movw	%ax, %es
	
	movw	$0x7c00, %sp

	xorw	%si, %si		# 将si置0,我们打算每触发一次中断就在屏幕上打印一个字符,通过si控制打印位置。
	call	install_ivt		# 调用安装中断向量表的例程

# 初始化 8259a
# 使用默认配置


sleep:
	hlt				# 通过hlt指令使处理器停止执行指令,并处于停机状态。停机状态可以被中断唤醒,继续执行。
	jmp	sleep	

install_ivt:
	movw	$INIT_TYPE_CODE, %bx
	shlw	$2, %bx			# 根据中断号计算中断向量在中断向量表中的偏移地址。计算方法是左移2位,即乘4

	movw	$handler, (%bx)		# 将中断处理程序的段内偏移写入中断向量对应的偏移地址的前两个字节
	movw	$INIT_HANDLER_BASE, 2(%bx)	#  将中断处理程序所在的段地址写入中断向量对应的偏移地址的后两个字节。
	ret


# 断处理程序。只打印了一个字符,然后通过iret返回。
handler:
	movw	$'B' | 0x0a00, %es:(%si)
	addw	$2, %si			# 将索引移动到下一个位置。

	# send eoi
	movb	$0x20, %al
	outb	%al, $_8259A_MASTER	# 向8259a主片发送中断结束命令0x20,使8259a可以继续接收中断信号
	iret

	.org	510
	.word	0xAA55

运行

1
2
3
$ as --32 boot.s -o boot.o
$ objcopy -O binary -j .text boot.o boot.bin
$ qemu-system-i386 boot.bin

中断每隔54.925 ms触发一次,屏幕上也会每隔54.925 ms打印一次字符。这个示例程序中我们没有控制si的大小,在运行的时候要注意这一点。

示例三

该示例演示外部中断,并且重新设置了8259a

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
.code16

# 设置了两个符号常量,类似于c语言中的#define。
.set	INIT_TYPE_CODE, 0x20		# 0x08。这个中断号在BIOS对8259a做过初始化之后是分配给主片的0级中断的,这个引脚用于连接8254可编程定时/计数器。
					# 8254在被BIOS初始化后会每隔54.925 ms向这个引脚输出1个信号。

.set	INIT_HANDLER_BASE, 0x07c0	# 表示中断处理程序所在的段地址。

.set	_8259A_MASTER, 0x20		# 8259a的主片0x20端口。分配给8259a主片的端口是0x20、0x21,从片的端口是0xa0, 0xa1。
					# 这个示例中我们不对8259a进行编程,但是在中断处理完成之后需要通过0x20告诉主片这个中断已经处理完了。
					# 如果中断来自从片的话那就需要同时向主片,从片发送处理完成的信号。
.set	_8259A_SLAVE, 0xa0		# 

	movw	$0xb800, %ax
	movw	%ax, %es
	
	movw	$0x7c00, %sp

	xorw	%si, %si		# 将si置0,我们打算每触发一次中断就在屏幕上打印一个字符,通过si控制打印位置。

	call	install_ivt		# 调用安装中断向量表的例程


	# 初始化 8259a
	call init_8259a


sleep:
	hlt				# 通过hlt指令使处理器停止执行指令,并处于停机状态。停机状态可以被中断唤醒,继续执行。
	jmp	sleep	

install_ivt:
	movw	$INIT_TYPE_CODE, %bx
	shlw	$2, %bx

	movw	$handler, (%bx)		# 将中断处理程序的段内偏移写入中断向量对应的偏移地址的前两个字节
	movw	$INIT_HANDLER_BASE, 2(%bx)	#  将中断处理程序所在的段地址写入中断向量对应的偏移地址的后两个字节。
	ret

# 开始的子例程用于初始化8259a
# 8259a的初始化方式是依次写入初始化命令字ICW1-4,这个顺序是固定的。其中ICW1通过0x20端口写入(从片通过0xa0),ICW2-4通过0x21端口写入(从片通过0xa1)。
init_8259a:
	# ICW1
	movb $0x11, %al			# 通过向主片、从片写入0x11来开始初始化的过程。
					# 基本上在IBM PC/AT机中是固定写入0x11的,表示中断请求是边沿触发、多片8259a级联并且需要发送 ICW4。
	outb %al, $_8259A_MASTER
	outb %al, $_8259A_SLAVE

	# ICW2
	movb $0x20, %al			# 设置主片中断号从0x20(32)开始
	outb %al, $_8259A_MASTER + 1
	movb $0x28, %al			# 设置从片中断号从0x28(40)开始。
	outb %al, $_8259A_SLAVE + 1

	# ICW3
	movb $0x04, %al			# 设置主片IR2引脚连接从片。
	outb %al, $_8259A_MASTER + 1
	movb $0x02, %al			# 告诉从片输出引脚和主片IR2号相连。
	outb %al, $_8259A_SLAVE + 1

	# ICW4
	movb $0x01, %al			# 设置主片和从片按照8086的方式工作。
	outb %al, $_8259A_MASTER + 1
	outb %al, $_8259A_SLAVE + 1

	# OCW1
	movb $0x0, %al			# 设置主从片允许中断。
	outb %al, $_8259A_MASTER + 1
	outb %al, $_8259A_SLAVE + 1

	ret

# 断处理程序。只打印了一个字符,然后通过iret返回。
handler:
	movw	$'C' | 0x0a00, %es:(%si)
	addw	$2, %si			# 将索引移动到下一个位置。

	# OCW2
	movb	$0x20, %al 		# send eoi
	outb	%al, $_8259A_MASTER	# 向8259a主片发送中断结束命令0x20,使8259a可以继续接收中断信号
	iret

	.org	510
	.word	0xAA55

运行

1
2
3
$ as --32 boot.s -o boot.o
$ objcopy -O binary -j .text boot.o boot.bin
$ qemu-system-i386 boot.bin

运行结果和上一个示例类似。

参考文章