02_中断与系统调用
在计算机体系结构中,中断是 CPU 打破当前正常执行流,转而处理突发事件或请求的一种核心机制。按触发源和存在形式,中断严格划分为两大类:软件中断与硬件中断。
1. 软件中断 (Software Interrupt)
由程序员在代码中主动发起,指令本身真实存在于程序的内存空间中。
-
典型场景: 应用程序向操作系统请求特权服务(系统调用)。
-
示例:
INT 21H(DOS 系统调用)、INT 10H(BIOS 中断)。程序执行到这些明确写在内存中的指令时,主动触发中断流程。
2. 硬件中断 (Hardware Interrupt)
由外部硬件事件异步触发,由 CPU 在两条指令执行的间隙主动发起,中断指令本身并不存在于当前执行流的内存空间中。
-
典型场景: 外设状态变化(如键盘敲击、定时器到期、网络数据包到达)。
-
示例:
-
INT 8H:时钟中断(由可编程定时器 PIT 触发,典型频率为每秒 18.2 次)。 -
INT 9H:键盘中断(用户每次按下或松开按键时,由键盘控制器触发)。
-
硬件中断隐式执行示例:
mov ax, 0 ; 假定这条指令消耗了 1/18 秒
; ---> CPU 会在此处隐式插入并调用 INT 8H 时钟中断处理程序。
; 注意:调用 INT 8H 的指令并不在用户程序的代码内存中。
mov bx, 1
again:
add ax, bx
; ---> 假设用户此时敲击了某个键。
; ---> CPU 会在此处隐式插入并调用 INT 9H 键盘中断处理程序。
cmp ax, 100
jb again
二、 中断的底层执行机制 (INT 与 IRET)
无论是软件中断还是硬件中断,CPU 在响应中断时,都会执行一套标准且不可打断的硬件级上下文切换流程。在 16 位实模式下,以 INT n 指令为例,它的本质是一次受控的特权级跳转。
1. INT 指令的核心流程
当 CPU 决定执行 INT n(n 为 0~255 的中断号)时,硬件自动完成以下动作:
-
保存标志寄存器:
pushf(将标志寄存器 FL 压入堆栈)。 -
保存代码段:
push cs。 -
保存指令指针:
push ip(压入中断返回后下一条指令的偏移地址)。 -
跳转执行:
jmp dword ptr 0:[n*4]。CPU 从固定在物理内存0x00000处的中断向量表 (IVT) 中查找到目标地址。中断n的向量(即处理程序的 CS:IP)固定保存在0:[n*4]的双字内存单元中。
2. IRET 中断返回流程
中断处理程序执行完毕后,必须以 IRET (Interrupt Return) 指令结尾。CPU 在执行 IRET 时自动完成出栈恢复动作,顺序与压栈完全相反:
-
恢复指令指针:
pop ip。 -
恢复代码段:
pop cs。 -
恢复标志寄存器:
popf。
完整机制代码演示:
; --------- 调用方上下文 ---------
mov ah, 2
mov dl, 'A'
; 当 CPU 执行下面的 INT 21H 指令时,自动执行:
; 1. pushf (保存标志位)
; 2. push cs (保存当前代码段)
; 3. push offset back (保存下一条指令标号 back 的偏移地址 IP)
; 4. jmp dword ptr 0:[84h] (21H * 4 = 84H,即跳转到向量表中的目标地址)
int 21h
back:
mov ah, 4Ch
mov al, 0
int 21h
; --------- 中断处理程序上下文 ---------
; 假设 offset int_21h 已保存到 word ptr 0:[84h] 中
; 且 seg int_21h 已保存到 word ptr 0:[86h] 中
int_21h:
... ; 执行具体的 21H 中断逻辑
; 处理完毕,准备返回
; CPU 执行 IRET 时,自动执行:
; 1. pop ip (出栈赋给 IP,即回到 back 标号处)
; 2. pop cs (恢复原 CS)
; 3. popf (恢复原标志寄存器 FL)
iret
(注:在 32 位/64 位保护模式下,查表机制变为中断描述符表 IDT,压栈内容变为 EFLAGS、CS、EIP,且涉及 Ring 3 到 Ring 0 的权限校验与内核栈切换。)
三、 异常与专用硬件/系统中断
部分中断号被 CPU 硬件架构严格保留,用于处理执行流异常、硬件事件或系统调试。
| 中断号 | 名称 | 触发机制 / 作用 |
|---|---|---|
| INT 0 | 除零异常 | 当执行 DIV 或 IDIV 指令且除数为 0(或商溢出)时,CPU 硬件主动触发。 |
| INT 1 | 单步/陷阱中断 | 当标志寄存器中的 TF (Trap Flag) = 1 时,CPU 每执行完一条普通指令,就会自动触发一次 INT 1 中断。专门用于调试器(如 DOS 下的 debug)实现单步跟踪。 |
| INT 3 | 断点陷阱 | 唯一拥有单字节机器码 (CC) 的中断。专供调试器使用,用于在程序指令流中安全地插入软件断点,替换原指令以劫持执行流。 |
| INT 4 | 溢出中断 | 与特定指令 INTO 配合。若溢出标志位 OF = 1,执行 INTO 会触发 INT 4;若 OF = 0 则该指令无任何效果。 |
| INT 8 | 时钟中断 | 核心硬件中断。由主板上的定时器芯片(PIT)高频周期性触发。操作系统利用此中断进行系统计时、更新系统时间以及进行多任务的时间片轮转调度。 |
| INT 9 | 键盘中断 | 核心硬件中断。键盘控制器在按键被按下或松开时向 CPU 发送信号触发。处理程序会读取键盘端口数据,将其转换为扫描码/ASCII 码并存入键盘缓冲区。 |
四、 实模式经典软件中断生态
在 16 位实模式时代,软件中断是应用程序请求系统服务的唯一标准接口。通过向特定寄存器(如 AH)赋值功能号来实现路由。
1. INT 21H:MS-DOS 系统调用核心
进程与内存控制 (31H, 4CH, 62H)
| AH | 功能 | 传参 / 前置要求 | 返回值 / 结果 | 备注 |
|---|---|---|---|---|
| 4CH | 安全终止程序 | AL = 返回码 (Exit Code) |
结束程序,释放内存 | 任何 .EXE/.COM 程序的标准退出方式,控制权交还 command.com。 |
| 31H | 驻留退出 (TSR) | AL = 返回码DX = 需保留的内存大小 (以节 Paragraph,即 16 字节为单位) |
程序退出,控制权交还 DOS,但保留占用内存 | Terminate and Stay Resident。该程序在内存中不会被覆盖,常用于编写后台常驻程序。 |
| 62H | 获取 PSP 地址 | 无 | BX = PSP 的段地址 |
程序段前缀 (PSP) 是 DOS 加载程序时在程序首部创建的 256 字节数据区,包含重要的命令行参数、环境变量指针等运行时信息。 |
字符级 I/O 控制 (01H, 02H, 08H)
| AH | 功能 | 传参 | 返回值 | 备注 |
|---|---|---|---|---|
| 01H | 键盘输入(带回显) | 无 | AL = 字符 ASCII 码 |
程序会阻塞等待,按下的字符直接显示在屏幕上。 |
| 08H | 键盘输入(无回显) | 无 | AL = 字符 ASCII 码 |
同 01H,但屏幕不显示按键。常用于捕获控制键或编写密码掩码(如捕获输入后手动调用 02H 打印 *)。 |
| 02H | 屏幕输出单字符 | DL = 字符 ASCII 码 |
光标后移 | 若输出控制字符(如 0DH 回车,0AH 换行),屏幕物理光标会执行对应动作。 |
字符串级 I/O 控制 (09H, 0AH)
| AH | 功能 | 传参 | 行为说明 |
|---|---|---|---|
| 09H | 打印字符串 | DX = 字符串首地址 |
DOS 字符串打印机制不认 \0 (00H),只认 $ (24H)。遇到 $ 立刻停止打印。若需在屏幕上显示真实的 $ 符号,必须改用 02H 循环打印。 |
| 0AH | 缓冲输入字符串 | DX = 缓冲区首地址 |
DX 指向的内存必须严格遵循三段式缓冲结构(见下文详述)。 |
0AH 输入缓冲区的 3 段式物理结构要求:
-
第 0 字节 (入参): 程序员必须预先写入允许输入的最大字符数(包含最后的回车符)。
-
第 1 字节 (出参): DOS 写入实际输入的字符数(不含最后的回车符)。
-
第 2 字节开始 (数据区): 存放用户输入的真实字符序列,以回车符
0DH结尾。(注意:0AH 接收完成后,不会在末尾自动添加
$。若后续想用 09H 直接打印这段输入,程序员必须通过读取第 1 字节的长度,利用偏移量手动在0DH后写入一个 `$。)
2. INT 10H:BIOS 视频显存服务
直接绕过 DOS 与底层显卡硬件通讯,常用于编写无操作系统的引导扇区 (Bootloader) 或复古图形程序。
| AH | 功能 | 传参 / 前置要求 | 备注 |
|---|---|---|---|
| 00H | 设置显示模式 | AL = 模式号 |
屏幕会被清空。03H 为标准 80x25 文本模式;13H 为 320x200 256 色 VGA 图形模式。 |
| 02H | 设置光标位置 | BH = 页码DH = 行(Y)DL = 列(X) |
文本模式下将物理光标定位于特定坐标,屏幕左上角为 (0,0)。 |
| 0EH | 电传(TTY)输出 | AL = ASCII 码BH = 页码 |
Bootloader 打印字符串最常用的底层依赖。硬件会自动接管光标右移与屏幕底部的上滚逻辑。 |
| 13H | 显示格式字符串 | 传参极多(涉及 AL, BH, BL, CX, DH, DL 以及 ES:BP 指向字符串首址) | 支持输出带颜色属性的字符串。不依赖 $ 截断,而是通过 CX 明确指定长度,但由于占据大量寄存器,调用成本较高。 |
五、 系统调用的演进 (32/64位)
随着操作系统的发展和保护模式的普及,传统的通过 INT n 进行系统调用的方式由于性能开销过大被现代操作系统抛弃。
1. 传统 INT 调用的性能瓶颈
在保护模式下,执行 INT 指令触发特权级切换(从用户态 Ring 3 到内核态 Ring 0)需要极其繁琐的内存交互:
-
CPU 必须去物理内存中读取并解析 IDT 表(中断描述符表)。
-
进行复杂的权限安全检查(对比 DPL 与 CPL)。
-
强制切换堆栈(从用户栈切换到内核栈),并在新栈中压入大量上下文状态数据。
这种内存访问和权限校验消耗了数百个时钟周期,对于高频调用的接口(如网络收发、文件高频读写)构成了极大的性能瓶颈。
2. 过渡方案:Linux 32 位的 INT 80H
在早期的 32 位 Linux 中,全系统统一使用 INT 80H 作为系统调用唯一入口。
-
机制:
EAX存放系统调用号(Syscall Number,如 4 代表sys_write)。EBX,ECX,EDX,ESI,EDI,EBP依次存放最多 6 个参数。 -
现状: 目前仅为了兼容老旧的 32 位可执行程序而保留。
3. 现代方案:快速系统调用 (Fast System Call)
为了彻底绕过内存中的 IDT 表查表流程,Intel 和 AMD 在 CPU 硬件微架构内部引入了专用的 特定模块寄存器 (MSR, Model Specific Registers) 和全新的指令集:
-
x86 (32位) 方案:
SYSENTER/SYSEXIT(Intel 提出)。 -
x64 (64位) 标准:
SYSCALL/SYSRET(AMD 提出,现已成为 x86-64 的绝对通用标准)。
SYSCALL 的核心机制与优势:
-
绝对的零内存查找: 操作系统内核在引导启动时,预先将系统调用的 Ring 0 入口地址硬编码烧录到了 CPU 内部的高速专用寄存器(MSRs)中。
-
极速切换: 当用户态程序执行
SYSCALL指令时,CPU 硬件直接从 MSR 寄存器中提取内核入口和目标代码段。不查 IDT 表,跳过繁琐的特权级安全比对流程,瞬间切入 Ring 0。 -
纯寄存器传参: x64 Linux 中,系统调用号存入
RAX,参数严格按照RDI, RSI, RDX, R10, R8, R9的顺序存放于通用寄存器中,避免了压栈传参的内存访问开销。 -
性能跃升: 单次系统调用的硬件时钟周期消耗,从
INT 80H时代的数百个,暴降至几十个,极大地提升了现代操作系统的整体 I/O 吞吐能力。