汇编语言一发入魂 0x09 - 中断
中断
中断就是打断CPU
当前的执行流程,让CPU
去处理一下别的事情。当然,CPU
也可以选择拒绝。
中断的分类
中断按中断源可以分为内部中断
和外部中断
。
内部中断
内部中断可以由中断指令int
来触发,也可以是因为指令执行中出现了错误而触发,例如运算结果溢出会触发溢出中断;除法指令的除数为0
会触发除法出错中断。
外部中断
外部中断通过NMI
和INTR
这两条中断信号线接入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
,其他中断依次类推。
中断处理过程
保护断点的现场。先将标志寄存器
FLAGS
压栈,然后清除IF
位和TF
位。将当前的代码段寄存器cs
和指令指针寄存器ip
压栈。执行中断处理程序。将中断类型码乘以
4
(每个中断在中断向量表中占4
个字节),得到了该中断入口点在中断向量表中的偏移地址。从中断向量表中依次取出中断程序的偏移地址和段地址,分别替换ip
和cs
以转入中断处理程序执行。返回到断点接着执行。中断处理程序的最后一条指令必须是中断返回指令
iret
。iret
执行时处理器依次从堆栈中弹出ip、cs、flags
,于是处理器转到主程序继续执行。
下面我们通过几个例子感受一下。
实战
示例一
该示例演示内部中断。
代码
.code16
.set INT_TYPE_CODE, 0x70
.set INT_HANDLER_BASE, 0x07c0
movw $0xb800, %ax
movw %ax, %es
movw $0x7c00, %sp
# 安装中断向量表
call install_ivt
# 触发中断
int $INT_TYPE_CODE
jmp .
install_ivt:
movw $INT_TYPE_CODE, %bx
shlw $2, %bx
movw $handler, (%bx)
movw $INT_HANDLER_BASE, 2(%bx)
ret
# 中断处理程序
handler:
movw $'8' | 0x0a00, %es:0
iret
.org 510
.word 0xAA55
解释
第3、4
行设置了两个符号常量,类似于c
语言中的#define
。INT_TYPE_CODE
表示我们要使用的中断类型码,这个示例中我们打算手动触发0x70
号中断。INT_HANDLER_BASE
表示中断处理程序所在的段地址。
第12
行调用安装中断向量表的例程。
第15
行手动触发中断。
第20、21
行根据中断号计算中断向量在中断向量表中的偏移地址。计算方法是左移2
位,即乘4
。
第23
行将中断处理程序的段内偏移写入中断向量对应的偏移地址的前两个字节。
第24
行将中断处理程序所在的段地址写入中断向量对应的偏移地址的后两个字节。
第30、31
行是我们的中断处理程序。只打印了一个字符,然后通过iret
返回。
运行
$ as --32 boot.s -o boot.o
$ objcopy -O binary -j .text boot.o boot.bin
$ qemu-system-i386 boot.bin
示例二
该示例演示外部中断。
代码
.code16
.set INT_TYPE_CODE, 0x08
.set INT_HANDLER_BASE, 0x07c0
.set _8259A_MASTER, 0x20
movw $0xb800, %ax
movw %ax, %es
movw $0x7c00, %sp
xorw %si, %si
# 安装中断向量表
call install_ivt
# 初始化 8259a
# 使用默认配置
sleep:
hlt
jmp sleep
install_ivt:
movw $INT_TYPE_CODE, %bx
shlw $2, %bx
movw $handler, (%bx)
movw $INT_HANDLER_BASE, 2(%bx)
ret
handler:
movw $'8' | 0x0a00, %es:(%si)
addw $2, %si
# send eoi
movb $0x20, %al
outb %al, $_8259A_MASTER
iret
.org 510
.word 0xAA55
解释
第3
行我们将中断类型码改成了0x08
。这个中断号在BIOS
对8259a
做过初始化之后是分配给主片的0
级中断的,这个引脚用于连接8254
可编程定时/计数器。8254
在被BIOS
初始化后会每隔54.925 ms
向这个引脚输出1
个信号。
第5
行_8259A_MASTER
是8259a
的主片0x20
端口。分配给8259a
主片的端口是0x20、0x21
,从片的端口是0xa0, 0xa1
。这个示例中我们不对8259a
进行编程,但是在中断处理完成之后需要通过0x20
告诉主片这个中断已经处理完了。如果中断来自从片的话那就需要同时向主片,从片发送处理完成的信号。
第12
行将si
置0
,我们打算每触发一次中断就在屏幕上打印一个字符,通过si
控制打印位置。
第20~22
行通过hlt
指令使处理器停止执行指令,并处于停机状态。停机状态可以被中断唤醒,继续执行。
第35
行将索引移动到下一个位置。
第38、39
行向8259a
主片发送中断结束命令0x20
,使8259a
可以继续接收中断信号。
运行
$ as --32 boota.s -o boota.o
$ objcopy -O binary -j .text boota.o boota.bin
$ qemu-system-i386 boota.bin
中断每隔54.925 ms
触发一次,屏幕上也会每隔54.925 ms
打印一次字符。这个示例程序中我们没有控制si
的大小,在运行的时候要注意这一点。
示例三
该示例演示外部中断,并且重新设置了8259a
。
代码
.code16
.set INT_TYPE_CODE, 0x20
.set INT_HANDLER_BASE, 0x07c0
.set _8259A_MASTER, 0x20
.set _8259A_SLAVE, 0xa0
movw $0xb800, %ax
movw %ax, %es
movw $0x7c00, %sp
xorw %si, %si
# 安装中断向量表
call install_ivt
# 初始化 8259a
call init_8259a
sleep:
hlt
jmp sleep
install_ivt:
movw $INT_TYPE_CODE, %bx
shlw $2, %bx
movw $handler, (%bx)
movw $INT_HANDLER_BASE, 2(%bx)
ret
init_8259a:
movb 0x11, %al
outb %al, $_8259A_MASTER
outb %al, $_8259A_SLAVE
movb $0x20, %al
outb %al, $_8259A_MASTER + 1
movb $0x28, %al
outb %al, $_8259A_SLAVE + 1
movb $0x04, %al
outb %al, $_8259A_MASTER + 1
movb $0x02, %al
outb %al, $_8259A_SLAVE + 1
movb $0x01, %al
outb %al, $_8259A_MASTER + 1
outb %al, $_8259A_SLAVE + 1
movb $0x0, %al
outb %al, $_8259A_MASTER + 1
outb %al, $_8259A_SLAVE + 1
ret
handler:
movw $'8' | 0x0a00, %es:(%si)
addw $2, %si
# send eoi
movb $0x20, %al
outb %al, $_8259A_MASTER
iret
.org 510
.word 0xAA55
解释
第3
行我们将INT_TYPE_CODE
修改成了0x20
。这次我们会重新设置8259a
,将他的主片中断号设置为从0x20
开始。
第34
行开始的子例程用于初始化8259a
。8259a
的初始化方式是依次写入初始化命令字ICW1-4
,这个顺序是固定的。其中ICW1
通过0x20
端口写入(从片通过0xa0
),ICW2-4
通过0x21
端口写入(从片通过0xa1
)。
第35~37
行通过向主片、从片写入0x11
来开始初始化的过程。基本上在IBM PC/AT
机中是固定写入0x11
的,表示中断请求是边沿触发、多片8259a
级联并且需要发送 ICW4
。
第39、40
行设置主片中断号从0x20(32)
开始。
第41、42
行设置从片中断号从0x28(40)
开始。
第44、45
行设置主片IR2
引脚连接从片。
第46、47
行告诉从片输出引脚和主片IR2
号相连。
第49~51
行设置主片和从片按照8086
的方式工作。
第53~55
行设置主从片允许中断。
运行
运行结果和上一个示例一样,就不贴图了。
文中涉及了8259a
,但是并没有详细介绍相关的知识。因为网上的资料太丰富了,大家打开搜索引擎直接搜索就可以了。另外,现代的处理器一般使用APIC
来处理中断,即高级可编程中断控制器
。