中断与端口

中断是 CPU 对系统突发事件的处理机制,端口是 CPU 与外部硬件通信的窗口。这是汇编从”纯软件”走向”操作硬件”的关键。

前置知识

需要理解 01-寄存器 中的 CS:IP 和 FLAGS 寄存器,以及 07-转移指令与循环 的 jmp 远转移。

一、中断

什么是中断?

CPU 在执行程序过程中,遇到某个特殊情况(如除零错误、键盘按下、定时器到点),需要暂时停下当前程序,转去处理这个特殊情况,处理完后再回来继续执行原来的程序——这个机制就是中断

中断本质上是一种特殊的转移:CPU 收到中断信号后,根据中断类型码找到中断处理程序的入口地址,修改 CS:IP 转去执行,执行完后用 iret 返回。

中断类型码

8086 用中断类型码(0~255)来标识不同的中断来源:

类型码中断源
0除法错误(div 溢出)
1单步执行(debug 的 t 命令)
4溢出(into 指令触发)
nint n 指令触发的软件中断(n 由指令给出)
其他外部硬件中断(键盘、定时器等)

中断向量表

CPU 如何根据中断类型码找到处理程序?答案是中断向量表

中断向量表位于内存 0000:0000 ~ 0000:03FF(共 1KB),每个中断类型码对应一个表项,每个表项占 4 字节

  • 前 2 字节(低地址):偏移地址 IP
  • 后 2 字节(高地址):段地址 CS
中断类型码 N 对应的表项地址 = 0000:N×4
表项内容 = (CS):(IP),即中断处理程序的入口地址

中断过程

int n 指令为例,CPU 执行中断的过程:

  1. 取中断类型码 n
  2. 标志寄存器入栈(保存 FLAGS)
  3. 设置标志位:TF = 0,IF = 0(防止中断过程中又被中断)
  4. CS、IP 入栈(保存返回地址)
  5. 设置 CS:IP:IP = (n×4),CS = (n×4+2),从中断向量表读取处理程序地址
  6. 开始执行中断处理程序

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。

键盘输入的处理过程

键盘是典型的外中断案例:

  1. 按键时,键盘芯片产生扫描码代表按键位置:
    • 通码(Make Code):按下时产生
    • 断码(Break Code):松开时产生,值 = 通码 + 80H
  2. 扫描码被送入主板接口芯片的寄存器,端口地址 60h
  3. 触发 9号中断(键盘中断)
  4. CPU 调用 9 号中断处理程序,从 60h 端口读取扫描码
  5. 处理程序将扫描码转换为 ASCII 码,存入键盘缓冲区

BIOS 中断 vs DOS 中断

  • BIOS 中断(如 int 10h 视频、int 16h 键盘、int 13h 磁盘):固化在主板 ROM 中,由硬件厂商提供,不依赖操作系统
  • DOS 中断(如 int 21h):由 DOS 操作系统提供,程序加载后才可用

注意区分

int 19h(操作系统引导)是 BIOS 中断,不是 DOS 中断。它运行时 DOS 还没加载进内存,是它把 DOS 从磁盘读入内存的。

自定义中断处理程序

编写并安装自己的中断处理程序,步骤:

  1. 编写中断处理程序代码
  2. 将程序复制到一块不会被系统覆盖的内存区域(如 0000:0200 之后的空闲区域)
  3. 将入口地址写入中断向量表对应位置(0:n×4
  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

inout 指令中,数据必须放在 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。

相关