汇编 I/O 与中断:端口读写、中断向量表与 ISR
从端口读写到中断向量表,从 BIOS 调用到硬件 ISR。本文拆解 I/O 映射、中断处理流程,带你理解操作系统如何管理硬件设备。
I/O 端口
CPU 通过端口与外设通信。x86 有独立的 I/O 地址空间(64KB)。
"独立"是关键词:端口地址 60h 和内存地址 60h 是两个完全不同的东西,CPU 靠指令区分——操作内存用 MOV,操作端口用 IN/OUT。这套设计叫 I/O 映射(port-mapped I/O),区别于把设备寄存器直接映射到内存地址的 MMIO(现代显卡、网卡多用后者)。
DOS 运行在实模式,任何程序都能直接 IN/OUT。到了保护模式就不一样了:这两个指令是特权指令,只有当前特权级不低于 IOPL,或者 I/O 位图允许该端口时才能执行,否则触发异常。这也是为什么现代操作系统里操作硬件必须写驱动。
端口读写指令
IN AL, DX ; 从 DX 指向的端口读入 AL
OUT DX, AL ; 将 AL 写入 DX 指向的端口
规则不复杂:
- 端口号小于 256 时可以直接写立即数,比如
IN AL, 60h;超过 255 就必须先放进DX,所以DX是 16 位端口号的专用寄存器。 - 数据宽度可以是 8 位(
AL)或 16 位(AX),对应 8 位/16 位端口。 OUT的书写顺序是"先端口后数据",和直觉相反,记成"往哪儿、写什么"就好。
几个实模式下的常用端口,看代码时不至于完全陌生:
| 端口 | 用途 |
|---|---|
| 20h / 21h | 主片 8259 中断控制器(命令 / 屏蔽字) |
| 60h | 键盘控制器数据口(读扫描码) |
| 61h | 扬声器与键盘控制 |
| 3F8h | COM1 串口数据口 |
中断向量表(IVT)
内存 0000:0000 处的 1KB 空间,存放 256 个中断处理程序地址:
| 中断号 | 功能 |
|---|---|
| 00h | 除零错误 |
| 08h | 定时器中断 |
| 09h | 键盘中断 |
| 10h | BIOS 显示服务 |
| 13h | BIOS 磁盘服务 |
| 21h | DOS 系统调用 |
1KB = 256 × 4,每项 4 字节,前 2 字节是处理程序的偏移(IP),后 2 字节是段地址(CS)。所以查表公式是固定的:
中断 n 的入口地址 = 0000:(n*4) ; 低字 → IP
0000:(n*4+2) ; 高字 → CS
注意是"低字在前",因为 x86 是小端序。
表里 08h~0Fh 这 8 个中断不是软件随便定的,而是 8259 中断控制器把硬件 IRQ0~IRQ7 固定映射过来的:IRQ0 是定时器 → INT 08h,IRQ1 是键盘 → INT 09h。这个映射关系是 PC 体系结构的历史遗留,改不了。
中断处理流程
- CPU 收到中断请求
- 保存 FLAGS、CS、IP 到栈
- 从 IVT 查找处理程序地址
- 跳转到处理程序
IRET返回
补充几个第 2 步和第 3 步之间的细节:
- 压栈顺序是 FLAGS、CS、IP,所以进入处理程序时栈顶是返回的 IP,接着是 CS,再是 FLAGS。
IRET正好按相反顺序弹回来,这就是它和RET的区别(RET只弹 IP,或者 IP+CS)。 - 进入中断时 CPU 会自动清
IF,屏蔽同级和更低优先级的可屏蔽中断,避免处理程序被自己打断;要继续响应中断就得显式STI。 - 硬件中断的处理程序结束前必须给 8259 发 EOI(
MOV AL, 20h/OUT 20h, AL),否则 8259 认为这个中断还在处理中,同优先级及以下的中断会被一直挡住。软件中断(INT 21h这种)不走这条路,也就不需要 EOI。
自定义中断处理程序
; 安装自定义中断
CLI ; 关中断
MOV AX, 0
MOV ES, AX
MOV WORD PTR ES:[09h*4], OFFSET MyISR
MOV WORD PTR ES:[09h*4+2], CS
STI ; 开中断
; 中断服务程序
MyISR PROC
PUSH AX
; 处理键盘输入
POP AX
IRET
MyISR ENDP
逐行解释:
MOV AX, 0/MOV ES, AX:让 ES 指向段 0,因为 IVT 固定在物理地址 0 开始。实模式下访问ES:[...]就等于访问绝对地址。09h*4由汇编器在汇编期算成 24h,也就是第 9 项的偏移字段;+2就是同一项里的段字段。两个字段要分两次写,不能一次写 4 字节,因为偏移和段来自不同的段寄存器(OFFSET是当前段的偏移,CS是代码段)。CLI/STI夹住这两条写操作:如果在写完偏移、还没写段地址的时候来了键盘中断,CPU 会跳到"新偏移 + 旧段地址"这个不存在的入口。关中断只有几条指令的窗口,代价可以忽略。- 处理程序里
PUSH AX/POP AX是必须的:中断是"插队"执行的,被打断的代码并不知道自己的寄存器被改了。 IRET而不是RET,否则 FLAGS 和 CS 留在栈上,栈会越来越歪。
这个示例只够演示"怎么挂上去",真要用还有三件事必须做,否则键盘会失灵:
- 先保存原向量,处理完再转过去。 直接把 09h 换成自己的处理程序,DOS 就再也收不到键盘输入了。正确做法是把原来的 4 字节存下来,在处理程序末尾用远跳转(
JMP到旧偏移:旧段)交给原处理程序收尾,而不是自己IRET。 - 读走端口 60h。 键盘中断的起因是控制器里有扫描码,不读走它,中断条件一直成立。
- 不要在处理程序里调 DOS。 DOS 不可重入,中断随时可能打断一个正在进行中的 DOS 调用,此时再进 DOS 会破坏它的内部状态。要打印调试信息只能用 BIOS 的 INT 10h,并且要自己保证 DS 有效。
BIOS 中断
| 中断 | 功能号 | 用途 |
|---|---|---|
| INT 10h | AH=00h | 设置显示模式 |
| INT 10h | AH=0Eh | 显示字符(TTY) |
| INT 13h | AH=02h | 读磁盘扇区 |
| INT 16h | AH=00h | 读取键盘按键 |
这几项的具体参数值得记一下,因为后面的实例和实验都要用:
- INT 10h / AH=00h:
AL是模式号,常用的03h是 80×25 彩色文本,13h是 320×200×256 色图形。 - INT 10h / AH=0Eh:
AL是要显示的字符,BL在图形模式下表示颜色。它会自动处理回车、换行和滚屏,适合做最简单的输出。 - INT 13h / AH=02h:
AL=读取的扇区数,CH=柱面号,CL=起始扇区号(从 1 开始),DH=磁头号,DL=驱动器号(0 是软盘 A),ES:BX=数据缓冲区。成功时AH=0且CF=0,失败时AH是错误码。读盘是少数必须检查CF的操作——读失败了继续往下跑,得到的就是一堆垃圾数据。 - INT 16h / AH=00h:阻塞等待按键,返回
AH=扫描码、AL=ASCII 码。功能键(F1、方向键)没有 ASCII 码,AL为 0,只能靠扫描码区分。
总结
中断是硬件与软件的桥梁。理解中断机制,你就掌握了操作系统与硬件对话的语言。
下一篇:《多模块程序设计与混合编程:C 语言调用汇编》
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)