中断与端口
中断是 CPU 对系统突发事件的处理机制,端口是 CPU 与外部硬件通信的窗口。这是汇编从”纯软件”走向”操作硬件”的关键。
前置知识
需要理解 01-寄存器 中的 CS:IP 和 FLAGS 寄存器,以及 07-转移指令与循环 的 jmp 远转移。
一、中断
什么是中断?
CPU 在执行程序过程中,遇到某个特殊情况(如除零错误、键盘按下、定时器到点),需要暂时停下当前程序,转去处理这个特殊情况,处理完后再回来继续执行原来的程序——这个机制就是中断。
中断本质上是一种特殊的转移:CPU 收到中断信号后,根据中断类型码找到中断处理程序的入口地址,修改 CS:IP 转去执行,执行完后用 iret 返回。
中断类型码
8086 用中断类型码(0~255)来标识不同的中断来源:
| 类型码 | 中断源 |
|---|---|
| 0 | 除法错误(div 溢出) |
| 1 | 单步执行(debug 的 t 命令) |
| 4 | 溢出(into 指令触发) |
| n | int n 指令触发的软件中断(n 由指令给出) |
| 其他 | 外部硬件中断(键盘、定时器等) |
中断向量表
CPU 如何根据中断类型码找到处理程序?答案是中断向量表。
中断向量表位于内存 0000:0000 ~ 0000:03FF(共 1KB),每个中断类型码对应一个表项,每个表项占 4 字节:
- 前 2 字节(低地址):偏移地址 IP
- 后 2 字节(高地址):段地址 CS
中断类型码 N 对应的表项地址 = 0000:N×4
表项内容 = (CS):(IP),即中断处理程序的入口地址
中断过程
以 int n 指令为例,CPU 执行中断的过程:
- 取中断类型码
n - 标志寄存器入栈(保存 FLAGS)
- 设置标志位:TF = 0,IF = 0(防止中断过程中又被中断)
- CS、IP 入栈(保存返回地址)
- 设置 CS:IP:IP = (n×4),CS = (n×4+2),从中断向量表读取处理程序地址
- 开始执行中断处理程序
iret 指令:中断返回
中断处理程序最后用 iret 返回,它恢复之前保存的状态:
; iret 相当于:
pop IP
pop CS
popf ; 弹出标志寄存器
int/iret vs call/ret
- call/ret:只保存和恢复 CS:IP(远调用)或 IP(近调用)
- int/iret:额外保存和恢复 FLAGS,并自动清除 TF/IF
内中断(来自CPU内部)
内中断是 CPU 内部产生的中断:
- 除法错误(int 0):执行 div 时结果超出寄存器范围
- 单步执行(int 1):TF=1 时,每条指令执行完触发
- into 指令(int 4):溢出中断,OF=1 时执行 into 触发
- int n 指令:程序主动触发的软件中断
debug 的 t 命令就是把 TF 标志设为 1,让 CPU 每执行一条指令就触发单步中断,从而实现单步调试。
外中断(来自外部硬件)
CPU 通过芯片(如 8259A 可编程中断控制器)接收外部设备的中断信号。外中断分为两类:
可屏蔽中断
CPU 可以选择不响应的外中断。是否响应取决于 IF 标志:
- IF = 1:响应可屏蔽中断,执行完当前指令后处理
- IF = 0:忽略可屏蔽中断
sti 指令设置 IF=1(开中断),cli 指令设置 IF=0(关中断)。
不可屏蔽中断
CPU 必须立即响应的外中断(如内存错误、电源故障),中断类型码固定为 2。
键盘输入的处理过程
键盘是典型的外中断案例:
- 按键时,键盘芯片产生扫描码代表按键位置:
- 通码(Make Code):按下时产生
- 断码(Break Code):松开时产生,值 = 通码 + 80H
- 扫描码被送入主板接口芯片的寄存器,端口地址 60h
- 触发 9号中断(键盘中断)
- CPU 调用 9 号中断处理程序,从 60h 端口读取扫描码
- 处理程序将扫描码转换为 ASCII 码,存入键盘缓冲区
BIOS 中断 vs DOS 中断
- BIOS 中断(如
int 10h视频、int 16h键盘、int 13h磁盘):固化在主板 ROM 中,由硬件厂商提供,不依赖操作系统 - DOS 中断(如
int 21h):由 DOS 操作系统提供,程序加载后才可用
注意区分
int 19h(操作系统引导)是 BIOS 中断,不是 DOS 中断。它运行时 DOS 还没加载进内存,是它把 DOS 从磁盘读入内存的。
自定义中断处理程序
编写并安装自己的中断处理程序,步骤:
- 编写中断处理程序代码
- 将程序复制到一块不会被系统覆盖的内存区域(如 0000:0200 之后的空闲区域)
- 将入口地址写入中断向量表对应位置(
0:n×4) - 用
iret返回
; 示例:编写0号中断(除法溢出)的处理程序
do0:
; 显示 "overflow!"
; ...(往显存写字符串的代码)
mov ax, 4c00H
int 21H
do0end: nop ; 用 offset do0end - offset do0 计算程序长度
二、端口
什么是端口?
CPU 和外部硬件(键盘、显示器、网卡、CMOS 等)通信不能直接读写硬件的寄存器,而是通过端口(Port)这个抽象接口。
每个硬件设备控制器都有几个端口(I/O 地址),CPU 通过端口号访问它们。端口地址空间和内存地址空间是独立的,通过不同指令访问:
- 访问内存:
mov指令(M/IO引脚=高电平) - 访问端口:
in/out指令(M/IO引脚=低电平)
端口地址空间只有 64KB(0~65535),因为 in/out 只使用地址总线的低16位。
in 指令:从端口读数据
; 8位端口(0~FFH):端口号直接写在指令中
in al, 60H ; 从 60H 端口读一个字节到 AL
; 16位端口(0~FFFFH):端口号放在 DX 中
mov dx, 3F8H ; 串口端口号
in al, dx ; 从 DX 指定的端口读一个字节到 AL
in ax, dx ; 读一个字到 AX
out 指令:向端口写数据
; 8位端口
out 60H, al ; 将 AL 的值写入 60H 端口
; 16位端口
mov dx, 3F8H
out dx, al ; 将 AL 的值写入 DX 指定的端口
out dx, ax ; 将 AX 的值写入(一个字)
端口读写只能用 AL/AX
in和out指令中,数据必须放在 AL(8位)或 AX(16位)中,不能用其他寄存器!
CMOS RAM 的读取(经典案例)
CMOS RAM 是主板上一块小的 RAM,存放系统时间、配置信息等。它有 128 个存储单元,但只分配了两个端口:
- 70H:地址端口(要访问哪个单元)
- 71H:数据端口(读写那个单元的数据)
为什么不直接给 128 个单元各分配一个端口?因为端口空间有限,这种索引/数据窗口机制用两个端口就能访问大量内部单元。
为什么内存不用这种方式?
内存是用来”跑”程序的,需要高速随机访问,如果每次访问都先发地址再等数据,性能会大幅下降。内存直接连在 CPU 地址总线上,有 1MB 专用地址空间。端口是用来”下命令”的,访问频率低,节省端口空间更重要。
读取 CMOS 的步骤:
; 读取 CMOS RAM 中 2 号单元(秒)
mov al, 2 ; al = 要访问的单元号
out 70H, al ; 将单元号送入地址端口
in al, 71H ; 从数据端口读取该单元的数据
三、显存与字符显示
显存地址
80×25 彩色字符模式下,显示缓冲区(显存)位于内存地址 B8000H ~ BFFFFH(共 32KB)。
往这个地址空间写入数据,内容会立即显示在屏幕上。
显存结构
屏幕上每个字符占显存中 2 个字节:
| 字节 | 内容 |
|---|---|
| 偶地址(低字节) | 字符的 ASCII 码 |
| 奇地址(高字节) | 属性字节(颜色) |
属性字节格式(8位):
7 6 5 4 3 2 1 0
[BLK][ R G B ][ I R G B ]
闪烁 背景色 前景色
颜色编码(3位,0~7):
- 0=黑, 1=蓝, 2=绿, 3=青, 4=红, 5=品红, 6=棕, 7=白
- I 位(位3)是高亮位,设置后颜色变亮(如 0111=灰白,1111=亮白)
80×25 = 2000 个字符,每个字符 2 字节 = 4000 字节 ≈ 4KB,共 8 页(B800H~BFFFH 共32KB)。
; 在屏幕左上角显示一个绿底红字的 'A'
mov ax, 0B800H
mov es, ax
mov byte ptr es:[0], 'A' ; 字符
mov byte ptr es:[1], 24H ; 属性:绿底(010) 红字(100),IRGB=0100 0100B=24H?
; 实际:背景绿(010) 前景红(100) → 0010 0100B = 24H
debug 中查看显存的注意点
在 DOSBox 中用
d B800:0查看显存时,数据会不停跳动——因为 DOSBox 本身也在用显存显示命令行,你的写入会被 DOS 的输出覆盖。建议在写入后立即观察屏幕效果,而不是用 d 命令查看。
偏移量计算
屏幕第 row 行、第 col 列的字符地址:
偏移 = (row × 80 + col) × 2
行号 024,列号 079。
相关
- 01-寄存器 — CS:IP、FLAGS 寄存器
- 04-第一个程序 — int 21H 退出程序
- 07-转移指令与循环 — jmp 远转移
- 09-标志寄存器 — IF 和 TF 标志位详解