《汇编语言》- 读书笔记 - 第15章-外中断
- 15.1 接口芯片和端口
- 15.2 外中断信息
- 1. 可屏蔽中断(Maskable Interrupt)
- 2. 不可屏蔽中断(Non-Maskable Interrupt)
- 设计思想
- 15.3 PC 机键盘的处理过程
- 1. 键盘输入
- 2. 引发 9 号中断
- 3. 执行 int 9 中断例程
- 15.4 编写int9 中断例程
- 编程
- 思路
- 代码
- 检测点 15.1
- 15.5 安装新的 int 9 中断例程
- 分析
- 代码
- 运行效果
- 实验 15 安装新的 int9 中断例程
CPU
通过中断机制
得知外部
输入事件,并通过I/O接口
与数据总线
实现与外设的数据交换
,从而实现对外设输入的有效处理。
CPU 对外设输入的通常处理方法:
- 外设的输入送入端口;
- 向 CPU 发出外中断(可屏蔽中断)信息;
CPU
检测到可屏蔽中断信息;
3.1. 如果IF=1
,CPU 在执行完当前指令后响应中断,执行相应的中断例程;- 可在中断例程中实现对外设输入的处理。
端口和中断机制,是 CPU 进行 I/O 的基础。
15.1 接口芯片和端口
CPU
通过端口
和外部设备
进行联系。
15.2 外中断信息
外部设备
在完成一系列自己的工作后,需要将产生的数据
或状态
发给CPU
处理时,就会触发外中断
- 中断信息的发送过程大致如下:
-
中断请求信号:
外设芯片在适当时候会通过自身的中断输出引脚向主板上的中断控制器发送中断请求信号,这通常表现为一个电信号的变化。例如在8086体系结构中,外设可以通过INTR(Interrupt Request)线向CPU发出中断请求。 -
中断控制器:
主板上的中断控制器(如Intel 8259A可编程中断控制器)负责接收来自多个外设的中断请求,并根据它们的优先级和中断向量进行管理。中断控制器会将有效的中断请求打包并通过IRQ(Interrupt Request Line)线传输到CPU。 -
CPU响应中断:
当CPU在执行完当前指令后检查中断请求线时,如果发现有中断请求存在,它会暂停当前任务,保存现场,然后通过读取中断向量表(IVT)获取中断服务程序(ISR)的地址,跳转到相应的处理程序执行中断处理。 -
线路连接:
中断请求信号的传送是通过主板上的硬件连线完成的。例如在8086/8088系统中,外设与CPU间的中断请求线和中断响应线(INTA#)是主板上的物理连接线,它们属于系统总线的一部分,通过这些线路上的电信号变化来进行中断请求和响应的通讯。
注意,随着技术的发展,更现代的计算机系统可能采用了更为复杂和高级的中断控制器和总线标准,但核心原理仍然是外设通过特定线路发送中断信号给CPU,并由CPU依据一定的优先级策略来响应和处理中断。
1. 可屏蔽中断(Maskable Interrupt)
可屏蔽中断 | CPU
在收到可屏蔽中断
时会根据 CF
的状态来决定:执行完当前指令
后是否响应
。
IF 状态 | 响应 | 不响应 |
---|---|---|
IF = 1 | ✅ | |
IF = 0 | ✅ |
- 中断过程
可屏蔽中断
的中断过程
除了中断类型码是通过数据总线
从外部
传进CPU
的,其它与内中断相同。
我们可以通过指令(STI
, CLI
)设置IF
来控制是否响应可屏蔽中断
。
———— 指 令 ———— | — 功 能 — | 描述 |
---|---|---|
cli (Clear Interrupt Flag) | IF=0 禁止中断 | CPU 执行cli 将IF=0 后,仅保留对不可屏蔽中断(NMI) 的响应,直至遇到STI 指令为止。此操作有助于保护在执行重要且不能被打断的代码时,不被中断处理程序打乱。 例如: 在更改栈指针(SS:SP)时,需要防止因中断处理程序介入而破坏栈的状态。 |
sti (Set Interrupt Flag) | IF=1 允许中断 | CPU 执行sti 将IF=1 后,重新开启中断响应 功能。CPU会再次响应挂起 的中断请求,恢复正常的中断处理流程。这对于完成关键区域操作后恢复系统的正常中断响应至关重要。 |
- 在IF=0期间,中断请求不会丢失,但会被延迟处理,等待CPU重新打开中断响应后,按照中断控制器维护的队列顺序逐一处理。是否超时以及超时后的处理方法取决于设备本身的特性以及操作系统的调度策略。
硬件中断控制器(如Intel 8259A可编程中断控制器)负责管理和维持中断请求的排队。 CLI
与STI
结合运用,构建短暂的中断防护区域,确保其间代码免受外部中断影响,待关键操作完成后通过STI
恢复中断响应,保障系统并发处理能力和实时响应性能。在多任务OS中,内核常在切换进程环境、管控硬件资源等关键场景调用这两条指令。
2. 不可屏蔽中断(Non-Maskable Interrupt)
当不可屏蔽中断发生时,CPU会立即停止当前的执行流程,保存必要的上下文信息,然后跳转到预设的NMI处理程序去执行相应的处理代码。
即使在 CPU 执行 CLI 指令 禁止了可屏蔽中断 的情况下也不例外。
由于NMI的重要性,其处理过程一般要求快速、简洁,尽量减少对系统运行的影响。
- NMI触发的原因可能包括但不限于以下几种情况:
- 硬件故障:如电源故障、内存错误、严重硬件故障等;
- 系统调试需求:在调试环境中,开发者可能通过人工或硬件手段触发NMI,以便快速进入调试模式;
- 监控程序:某些嵌入式系统或服务器中,监控程序可能会通过NMI报告严重的系统错误或警告;
- 性能计数器溢出:某些高性能处理器中,性能监控单元(PMU)的计数器溢出可能导致NMI。
设计思想
可屏蔽中断:几乎都是外设触发的。告诉CPU来活了。
不可屏蔽中断:是发生了必须紧急处理的事件。CPU要先解决它了再说别的。
15.3 PC 机键盘的处理过程
1. 键盘输入
键盘上每个键分:按下
、松开
两种状态都会产生相应的扫描码
。
按下时的叫通码
,松开时的叫断码
。
它会被送入主板上的相关接口芯片的寄存器中,该寄存器的端口
地址为 60h
断码 = 通码 + 80h
比如: g 键的通码为 22h,断码为 a2h。
表 15.1 是键盘上部分键的扫描码,只列出通码。
2. 引发 9 号中断
60h
端口收到信号,相关的芯片就会向 CPU
发出中断类型码
为9
的可屏蔽中断
信息。
CPU
收到后,如果 IF=1
,则响应中断,引发中断过程
,转去执行 int 9
中断例程。
3. 执行 int 9 中断例程
BIOS 提供 int 9
中断例程,处理基本的键盘输入:
- 首先,中断例程通常会从60h端口读取扫描码。
- 处理扫描码
2.1. 若是字符键,则将扫描码转换为
ASCII码,存入
键盘缓冲区。 2.2. 若是
功能键或
控制键,则可能更新内存中的
键盘状态字节`或其他内部状态。 - 向键盘控制器发送应答信号,表明中断已被处理。
BIOS键盘缓冲区:
BIOS键盘缓冲区
是系统启动时BIOS设置的一块内存区域,用于存储15
个键盘输入事件,每个事件占用2字节
,其中高位
字节为扫描码
,低位
字节为字符码
。- 这个缓冲区通过
int 9
中断机制收集并暂存键盘输入,确保在操作系统尚未完全加载或正忙时也能记录用户的按键动作。
字单元(1) | 字单元(2) | … | 字单元(15) |
---|---|---|---|
高位-扫描码 | 高位-扫描码 | … | 高位-扫描码 |
低位-字符码 | 低位-字符码 | … | 低位-字符码 |
0040:17
单元存储键盘状态字节,该字节记录了控制键和切换键的状态。字节各位信息如下。
位 | 状态 |
---|---|
0 | 右 Shift 状态,置1表示按下右 Shift 键: |
1 | 左 Shitt 状态,置1表示按下左 Shit 键; |
2 | Ctrl 状态,置1表示按下 Ctrl 键; |
3 | Alt 状态,置1表示按下 Alt 键; |
4 | ScrollLock状态,置1表示 Scroll 指示灯亮; |
5 | NumLock 状态,置1表示小键盘输入的是数字; |
6 | CapsLock 状态,置1表示输入大写字母: |
7 | Insert 状态,置1表示处于删除态。 |
15.4 编写int9 中断例程
键盘输入的处理过程:
- 键盘产生扫描码;
- 扫描码送入
60h
端口; - 引发
9
号中断; - CPU 执行
int 9
中断例程处理键盘输入。
前三步都是硬件系统的活,我们能插手的就只有第4步。但因为 int 9
中断处理程序要与一些硬件细节打交到。所以我们只对 int 9
做下封装扩展。
编程
在屏幕中间依次显示“a”~“z”
,并可以让人看清。在显示的过程中,按下 Esc
键后,改变显示的颜色。
思路
- 先循环显示
a ~ z
。(为了看清,书上给的方案是空循环模拟延迟效果) - 自己写一个
int 9
代替原版。
2.1. 我们自己的int 9
中调用原版 int 9
。
2.2. 拿到原版int 9
返回的扫描码。
2.3. 判断如果是Esc
修改显存改变颜色
。
代码
assume cs:code
stack segment
db 128 dup(0)
stack ends
data segment
oint9 dw 0,0
data ends
code segment
start: mov ax,stack ; 设置栈段和栈顶位置
mov ss,ax
mov sp,128
mov ax,data ; 设置数据段
mov ds,ax
mov ax,0 ; 设置附加段
mov es,ax
; ------------ 保存原 int 9 中断列和入口到 ds:0, ds:2 ------------
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds: [2]
; ---------------- 将我们的新 int 9 写入中断向量表 ----------------
mov word ptr es:[9*4],offset int9
mov es:[9*4+2],cs
; ------------------------- 显示 a 到 z -------------------------
mov ax,0b800h ; 设置显存段
mov es,ax
mov ah,'a' ; 要显示的字符串,从 a 开始
s: mov es:[160*12+40*2],ah ; 显示字符
call delay ; 调用子程序:延时
inc ah ; 下一个字符
cmp ah,'z' ; 如果不是z继续循环
jna s
; ------------- 将中断向量表中 int 9恢复为原来的地址 -------------
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
mov ax,4c00h
int 21h
; =======================================================
; --------------------- 子程序 delay -------------------
; 让CPU空循环,模拟延时效果
; -------------------------------------------------------
; 参数: 无
; 返回: 无
; -------------------------------------------------------
delay:
push ax ; 备份寄存器
push dx
mov dx,2h ; 循环 2h 次,可以自己把握
mov ax,0
delays: sub ax,1
sbb dx,0 ; 10000000h 循环递减
cmp ax,0 ; 直到 ax, dx 都为 0 才跳出循环
jne delays
cmp dx,0
jne delays
pop dx ; 还原寄存器
pop ax
ret ; 返回
; ---------------------- 子程序 delay -------------------
; =======================================================
; =======================================================
; --------------------- 子程序 int 9 -------------------
; 调用原 int 9 获取扫描码,实现按 Esc 变色
; -------------------------------------------------------
; 参数: 无
; 返回: 无
; -------------------------------------------------------
int9:
push ax ; 备份寄存器
push dx
push es
in al,60h ; 从60h端口读取数据
pushf
pushf
pop bx
and bh,11111100b
push bx
popf
call dword ptr ds:[0] ; 调用原来的 int 9 中断例程
cmp al,1
jne int9ret
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
int9ret:pop es ; 还原寄存器
pop dx
pop ax
iret ; 返回
; --------------------- 子程序 int 9 ------------------
; =======================================================
code ends
end start
检测点 15.1
《汇编语言》- 读书笔记 - 各章检测点归档 - 检测点 15.1
15.5 安装新的 int 9 中断例程
任务 | 安装一个新的 int9 中断例程。 |
---|---|
功能 | 在 DOS下,按F1键后改变当前屏幕的显示颜色,其他的键照常处理 |
分析
- 改变屏幕的显示颜色。
改变从B8000H
开始的4000
个字节中的所有奇
地址单元中的内容
mov ax,0b800h
mov es ax
mov bx,1
mov cx,2000
s: inc byte ptr es:[bx]
add bx,2
loop s
-
按键常规处理
直接调用原 int 9 -
原 int9 入口地址
不能保存在安装程序中。我们把它放在0:0200
。 -
安装新 int9 中断例程
0:0200 ~ 0:0203
用来保存原 int9 的地址了。
我们保存新 int9 时从0:0204
开始。
代码
assume cs:code
stack segment
db 128 dup(0)
stack ends
code segment
start: mov ax,stack ; 设置栈段和栈顶位置
mov ss,ax
mov sp,128
; -------- 安装: 复制中断例程到目标内存 -------
mov ax,cs ;设置 ds:si 指向源地址
mov ds,ax
mov si,offset int9
mov ax,0 ;设置 es:di 指向目的地址
mov es,ax
mov di,204h
mov cx,offset int9end-offset int9 ;设置 cx为传输长度
cld ;设置传输方向为正。movsb中si,di递增
rep movsb ;重复复制数据次数由 cx 控制
; -------- 安装: 复制中断例程到目标内存 -------
; ---------- 备份原 int9 入口到 [0:200~0203] ----------
push es:[9*4]
pop es:[200h]
push es:[9*4+2]
pop es:[202h]
; ---------- 备份原 int9 入口到 [0:200~0203] ----------
; ---------- 设置中断向量表 ----------
cli ; 临时屏蔽中断
mov word ptr es:[9*4],204h ; 设置的偏移地址(0~3用来存原int9地址了)
mov word ptr es:[9*4+2],0 ; 设置的段地址
sti ; 恢复中断
; ---------- 设置中断向量表 ----------
ok: mov ax,4c00h
int 21h
; =======================================================
; --------------------- 子程序 int 9 -------------------
; 调用原 int 9 获取扫描码,实现按 Esc 变色
; -------------------------------------------------------
; 参数: 无
; 返回: 无
; -------------------------------------------------------
int9:
push ax ; 备份寄存器
push bx
push cx
push es
in al,60h ; 从60h端口读取数据
; 模拟 int 指令,用 call 调用原 int 9
pushf ; 进入中断后 IF、TF已经是0 直接入栈即可
call dword ptr cs:[200h] ; 调用原来的 int 9 中断例程
cmp al,3bh ; 判断是否 F1 键
jne int9ret ; 如果不是直接结束
mov ax,0b800h ; 设置显存
mov es,ax
mov bx,1
mov cx,2000
s: inc byte ptr es:[bx]
add bx,2
loop s
int9ret:pop es ; 还原寄存器
pop cx
pop bx
pop ax
iret ; 返回
int9end:nop
; --------------------- 子程序 int 9 ------------------
; =======================================================
code ends
end start
运行效果
实验 15 安装新的 int9 中断例程
《汇编语言》- 读书笔记 - 第15章-外中断-实验15 安装新的 int9 中断例程