从端口读写到中断向量表,从 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扬声器与键盘控制
3F8hCOM1 串口数据口

中断向量表(IVT)

内存 0000:0000 处的 1KB 空间,存放 256 个中断处理程序地址:

中断号功能
00h除零错误
08h定时器中断
09h键盘中断
10hBIOS 显示服务
13hBIOS 磁盘服务
21hDOS 系统调用

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 体系结构的历史遗留,改不了。

中断处理流程

  1. CPU 收到中断请求
  2. 保存 FLAGS、CS、IP 到栈
  3. 从 IVT 查找处理程序地址
  4. 跳转到处理程序
  5. 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 留在栈上,栈会越来越歪。

这个示例只够演示"怎么挂上去",真要用还有三件事必须做,否则键盘会失灵:

  1. 先保存原向量,处理完再转过去。 直接把 09h 换成自己的处理程序,DOS 就再也收不到键盘输入了。正确做法是把原来的 4 字节存下来,在处理程序末尾用远跳转(JMP 到 旧偏移:旧段)交给原处理程序收尾,而不是自己 IRET。
  2. 读走端口 60h。 键盘中断的起因是控制器里有扫描码,不读走它,中断条件一直成立。
  3. 不要在处理程序里调 DOS。 DOS 不可重入,中断随时可能打断一个正在进行中的 DOS 调用,此时再进 DOS 会破坏它的内部状态。要打印调试信息只能用 BIOS 的 INT 10h,并且要自己保证 DS 有效。

BIOS 中断

中断功能号用途
INT 10hAH=00h设置显示模式
INT 10hAH=0Eh显示字符(TTY)
INT 13hAH=02h读磁盘扇区
INT 16hAH=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 语言调用汇编》

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐