第十二章 内中断
# 第十二章 内中断
什么是中断?如果你学习过高级编程语言,可以将中断理解为异常的特殊处理过程,就像 Java 里面的 Exception。
任何一个通用的 CPU,都具备一种能力,可以在执行完当前正在执行的指令之后,检测从 CPU 外部发送过来的或内部产生的一种特殊信息,并且可以立即对所收到的信息进行处理。这种特殊信息,我们可以称其为:中断信息。中断的意思是指,CPU 不在接着 (刚执行完的指令) 向下执行,而是转去处理这个特殊信息。
中断信息可以来自 CPU 内部和外部,这一章,我们主要讨论来自 CPU 内部的中断信息,我们称之为内中断。
# 12.1 内中断的产生
8086CPU 使用单元字节大小的数字来标识中断类型。
CPU 内部可能产生多种多样的中断,那么应该如何来标识是哪种中断呢,或者说我们如何确定中断源?
8086CPU 用称为中断类型码的数据来标识中断信息的来源。中断类型码为一个字节型数据,可以表示 256 种中断类型。以后,我们将产生中断信息的事件,即中断信息的来源,称之为中断源。
# 12.2 中断处理程序
处理中断信息的程序被称为中断处理程序。
# 12.3 中断向量表
中断发生后,CPU 要根据中断类型码去执行对应的中断处理程序?但如何根据 8 位的中断类型码得到中断处理程序的地址呢?
实际上,8086CPU 用 8 位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。中断向量表就是中断处理程序入口地址的列表,列表的下标索引(从 0 开始)即是中断类型码的值。中断向量表实际上是中断类型码与中断处理程序入口地址之间的一种映射关系。可以理解为高级编程语言中的 Map 集合。
8086CPU 中断向量表指定放在内存 0 处。每个表项占用 4 个字节,高位字存放段地址,低位字存放偏移地址。
# 12.4 中断过程
用中断类型码找到中断向量,并用它设置 CS 和 IP 的值,这个工作是由 CPU 的硬件自动完成的。CPU 硬件完成这个工作的过程被称为中断过程。中断过程完成后,CPU 就会开始执行中断处理程序。中断过程可以理解为中断环境的初始化。那么在 CPU 进行中断过程中需要准备哪些工作呢?概括来说,主要进行以下六步准备工作:
(1)(从中断信息中) 取得中断类型码;
(2)标志寄存器的值入栈 (因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中);
(3)设置标志寄存器的第 8 位 TF 和第 9 位 IF 的值为 0 (这一步的目的后面将介绍);
(4)CS 的内容入栈;
(5)IP 的内容入栈;
(6)从内存地址为中断类型码 4 和中断类型码 4+2 的两个字单元中读取中断处理程序的入口地址设置 IP 和 CS。
# 12.5 中断处理程序和 iret 指令
中断处理程序必须一直存储在指定内存中,以应对随时可能发生的中断事件。
中断处理程序的编写方法和子程序比较相似,下面是常规步骤:
(1)保存用到的寄存器;
(2)处理中断;
(3)恢复用到的寄存器;
(4)用 ret 指令返回。
iret 指令的功能用汇编语法描述为:
pop IP
pop CS
popf
2
3
2
3
在中断过程中,注意标志寄存器入栈和出栈的次序。入栈顺序是标志寄存器、CS、IP,出栈顺序与此相反。
# 12.6 除法错误中断的处理
除法错误将引发 0 号中断。至于为何是 0 号中断,我估摸着除法中断时人们最容易想到也最容易遇到的中断了吧。
# 12.7 编程处理 0 号中断
我们的需求是重新编写一个 0 号中断处理程序,它的功能是在屏幕中间显示 “overflow!”,然后返回到操作系统。
为了满足以上需求,需要做一下几件事情:
(1)编写可以显示 “overflow” 的中断处理程序:do0;
(2)将 do0 送入内存 0000:0200 处;
(3)将 do0 的入口地址 0000:0200 存储在中断向量表 0 号表项中。
程序的框架如下:
assume cs:code
code segment
start: do0安装程序
设置中断向量表
mov ax, 4c00h
int 21h
do0: 显示字符串"overflow"
mov ax, 4c00h
int 21h
code ends
end start
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
下面摘抄书中比较精辟的一段总结:
我们如何让一个内存单元成为栈顶?将它的地址放入 SS、SP 中;
我们如何让一个内存单元中的信息被 CPU 当做指令来执行?将它的地址放入 CS、IP 中; 我们如何让一个内存单元成为要处理的数据?将它的段地址放在 DS 中;(书中无这句话,个人根据理解补充)
那么,我们如何让一段程序成为 N 号中断的中断处理程序呢?将它的入口地址放入中断向量表的 N 好表项中。
# 12.8 安装
所谓安装就是将中断处理程序 (do0) 送到指定内存处。
我们可以使用 movsb 指令,将 do0 的代码送入 0:200 处。复习一下 movsb 的用法:movsb 是串传送指令,其功能是将 ds:si 指向的内存单元中的字节送入 es:di 中,并根据标志寄存器 df 的值,将 si 和 di 递增或递减。movsb 指令往往与 rep 指令配合使用来实现批量字符串的传送。
安装程序的框架如下所示:
assume cs:code
code segment
start: 设置es:di指向目的地址
设置ds:si指向源地址
设置cx为传输长度
设置传输方向为正
rep movsb
设置中断向量表
mov ax, 4c00h
int 21h
do0: 显示字符串"overflow!"
mov ax, 4c00h
int 21h
code ends
end start
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
以上步骤的难点在于如何确认中断处理程序 do0 的长度?最笨的方法是计算 do0 中每句代码的长度,然后累加,但这样做太麻烦了,不仅要知道每行代码所占的字节数,代码稍有改动那就令人抓狂。书中作者给出一个非常简便的计算方式,利用编译器来帮助我们计算 do0 的长度。之前我们学过 offset 指令,他的功能是取得标号的偏移地址,我们在 do0 后面在添加一个标号 do0end,使用 offset do0end - offset do0 即可计算出 do0 的长度。
解决了字符传送以及确认 do0 长度这两个拦路虎后,我们就可以看一下较为完整的安装程序代码了:
assume cs:code
code segment
start:
mov ax, cs
mov ds, ax
mov si, offset do0 ;设置ds:si指向源地址
mov ax, 0
mov es, ax
mov di, 0200h ;设置es:di指向目标地址
mov cx, offset do0end - offset do0 ;设置cx为传输长度
cld ;设置传输方向为正
rep movsb ;传输开始
设置中断向量表
mov ax, 4c00H
int 21H
do0: 显示字符串"overflow!"
mov ax, 4c00h
int 21h
code ends
end start
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
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
这里补充一点,像 "+"、"-"、"*"、"/"、"offset" 这类指令都是伪指令,并不是标准的汇编指令,它是由编译器识别并由编译器翻译为对应的汇编指令。
# 12.9 do0
do0 程序即是我们的 0 号中断处理程序。其主要目的是显示字符串 "overflow!"。
主要程序代码如下所示:
do0: jum short do0start
db 'overflow!'
do0start: mov ax, cs
mov ds, ax
mov si, 202h
mov ax, 0b800h
mov es, ax
mov di, 12*160 + 36*2
mov cx, 9
s:mov al, [si]
mov es:[di], al
inc si
add di, 2
loop s
mov ax, 4c00h
int 21h
do0end: nop
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这部分代码需要注意的地方是,我们在子程序 do0 开始处定义了字符串 "overflow!",但它并不是可以执行的代码,所以在 "overflow!" 之前加上一条 jmp 指令,转移到正式的 do0 程序。
# 12.10 设置中断向量
设置中断向量,也即是将中断处理程序 do0 在内存中的入口地址存放在中断向量表 0 号表项中。0 号表项的地址为 0:0,其中 0:0 字单元存放中断处理程序入口地址的偏移地址,0:2 字单元存放中断处理程序入口地址的段地址。程序如下:
mov ax, 0
mov es, ax
mov word ptr es:[0*4], 200h
mov word ptr es:[0*4+2], 0
2
3
4
2
3
4
# 12.11 单步中断
基本上,在 CPU 执行完一条指令之后,如果检测到标志寄存器的 TF 位为 1,则产生单步中断,引发中断过程。单步中断的中断类型码为 1。在一开始我们说 CPU 在执行中断处理程序之前要先将标志寄存器 TF 位置 0,这就是为了防止 CPU 在执行 1 号类型中断 (单步中断) 时无限递归执行中断。
CPU 提供单步中断功能的出发点是,为单步跟踪程序的执行过程,提供了实现机制。
# 12.12 响应中断的特殊情况
一般情况下,CPU 在执行完当前指令后,如果检测到中断信息,就响应中断,引发中断过程。可是,在有些情况下,CPU 执行完当前指令后,即便是发生中断,也不会响应。例如针对 ss 修改执行后,下一条指令 (一般是修改 sp) 也会紧接着执行,中间即使发生中断,CPU 也不会去响应。这样做的主要原因是,ss:sp 联合指向栈顶,而对它们的设置应该连续完成。如果在执行完设置 ss 的指令后,CPU 响应中断,引发中断过程,要在栈中压入标志寄存器、CS 和 IP 的值,而 ss 改变,sp 并未改变,ss:sp 指向的不是正确的栈顶,将引起错误。
这种理念在高级编程语言中的具体体现是 “原子操作”,即一组操作要么不执行,要么就一次执行完毕,不会存在中间状态。
上课没有时间详细记笔记,复习阶段参考了 sanmianti/AssemblyLanguageTest (opens new window),感谢。