跳至主要內容

汇编语言一发入魂 0x09 - 中断

未央大约 7 分钟汇编语言中断8259a

中断

中断就是打断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,于是处理器转到主程序继续执行。

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

实战

示例一

该示例演示内部中断。

代码

.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语言中的#defineINT_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
boot
boot

示例二

该示例演示外部中断。

代码

.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。这个中断号在BIOS8259a做过初始化之后是分配给主片的0级中断的,这个引脚用于连接8254可编程定时/计数器。8254在被BIOS初始化后会每隔54.925 ms向这个引脚输出1个信号。

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

12行将si0,我们打算每触发一次中断就在屏幕上打印一个字符,通过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
boota
boota

中断每隔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行开始的子例程用于初始化8259a8259a的初始化方式是依次写入初始化命令字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来处理中断,即高级可编程中断控制器