汇编语言:中断例程的调用
int 指令
在 8086 CPU 上程序员可以用int 指令主动发起内中断,其格式为 int n,其中 n 为中断类型码。CPU 执行该指令时会立即触发一个中断过程:取类型码 n,标志寄存器入栈并清除 TF 与 IF,随后 CS 和 IP 入栈,最后根据 n 从中断向量表中取出入口地址跳转执行。该过程和除法溢出或单步中断在硬件流程上是一致的,区别是触发源来自指令而非硬件异常。
例如对于我们编写的 d0 中断处理程序,可将测试指令 “mov ax, 1000h、mov bh, 1、div bh” 替换为 int 0。
assume cs:code
code segment
start:
mov ax, cs
mov ds, ax
mov si, offset do0 ; 源:代码段中的 do0
mov ax, 0
mov es, ax
mov di, 200h ; 目的:0000:0200
mov cx, offset do0end - offset do0
cld
rep movsb ; 把 do0 代码复制到常驻区
; 设置 0 号中断向量
mov ax, 0
mov es, ax
mov word ptr es:[0], 200h ; 偏移地址
mov word ptr es:[2], 0 ; 段地址
int 0 ; 主动触发 0 号中断
mov ax, 4c00h
int 21h
do0:
jmp short do0start
db "overflow!"
do0start:
mov ax, cs
mov ds, ax
mov si, 202h ; 字符串紧跟在 jmp 之后
mov ax, 0b800h
mov es, ax
mov di, 12 * 160 + 36 * 2 ; 第 12 行第 36 列
mov cx, 9
s:
mov al, [si]
mov ah, 00000100b ; 设置字体颜色为红色
mov es:[di], ax
inc si
add di, 2
loop s
mov ax, 4c00h
int 21h
do0end:
nop
code ends
end start
执行后屏幕第 12 行附近同样显示了 “overflow!”,可见使用 int 指令将 0 号中断主动触发了。

int 指令的本质功能与 call 指令很相似,都是调用一段程序,但它提供了更为底层的系统调用机制。例如,在执行 int 0 时,即使程序中没有发生除法错误,CPU 也会强制转去执行 0 号中断向量指向的处理程序。这意味着我们可以通过 int 指令主动利用系统或 BIOS 预置的功能例程。更进一步,程序员可以将自己编写的功能模块以中断例程(Interrupt Routine)的形式安装在中断向量表中,供其他应用程序像调用 API 一样通过 int n 进行调用。
因为
int涉及查表(IDT 而非 IVT)和特权级切换,开销较大。在现代 x86 架构中,直接使用int指令调用系统服务的方式已被更高效的 syscall/sysenter 指令取代,不过中断描述符表(IDT)的概念依然是从 8086 的 中断向量表演化而来的。此外,现代操作系统(如 Windows 和 Linux)严格保护中断向量表,用户态程序无法直接修改 IDT,必须通过内核模块(驱动程序)才能注册中断处理程序。
供程序调用的中断例程
计算平方
功能需求为计算一个 word 型数据的平方,结果存放在 DX:AX 中,参数为 (AX) = 待计算的数据。乘法操作结果可能超过 16 位,因此使用 mul 指令进行无符号乘法,结果存放在 DX:AX 中。中断例程的结尾使用 iret 指令,该指令将依次执行 pop IP、pop CS、popf,与中断发生时硬件自动压栈的顺序相反,从而恢复应用程序的执行现场。iret 指令 与 ret 的功能一致,只不过多了标志寄存器的恢复。
assume cs:code
code segment
start:
; 安装中断例程
mov ax, cs
mov ds, ax
mov si, offset sqr
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset sqrend - offset sqr
cld
rep movsb
; 设置 7Ch 号中断向量
mov ax, 0
mov es, ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
; 例程调用测试
mov ax, 3456h
int 7ch ; 调用中断例程计算平方
mov ax, 4c00h
int 21h
; 中断例程:计算平方
sqr:
mul ax
iret
sqrend:
nop
code ends
end start
运行后程序将 3456 传入 AX,通过 int 7ch 调用例程得到平方值 179,506,404(DX:AX = AB3:0CE4)。

转换字符串为大写
功能需求是将以 0 结尾的字符串转换为大写,参数为 DS:SI 指向字符串首地址。中断例程需要遍历字符串,读取字符至 CL,利用 CX 判断是否为结尾(0)。若不为 0,则通过 and byte ptr [si], 11011111b 将 ASCII 码的第 5 位清零,实现小写转大写。
assume cs:code, ds:data
data segment
db 'conversation', 0
data ends
code segment
start:
; 安装中断例程
mov ax, cs
mov ds, ax
mov si, offset capital
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset capitalend - offset capital
cld
rep movsb
; 设置 7Ch 号中断向量
mov ax, 0
mov es, ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
; 测试例程
mov ax, data
mov ds, ax
mov si, 0
int 7ch ; 调用中断例程转换字符串
mov ax, 4c00h
int 21h
; 中断例程:小写转大写
capital:
push cx
push si
change:
mov cl, [si]
mov ch, 0
jcxz ok ; 若为0则跳转结束
and byte ptr [si], 11011111b
inc si
jmp short change
ok:
pop si
pop cx
iret
capitalend:
nop
code ends
end start
运行程序后,数据段中的字符串 'conversation' 被中断例程修改为 'CONVERSATION'。

显示字符串
功能需求是显示以 0 结尾的字符串,参数为 DH = 行号、DL = 列号、CL = 颜色属性、DS:SI = 指向字符串首地址。之前我们写过 show_str 子程序,这里要做的只是改为用 iret 进行返回,然后将其安装到内存中。安装后运行到 iret 指令时将依次弹出 IP、CS 和标志寄存器,使 CPU 回到 int 7ch 的下一条指令继续执行。
assume cs:code, ds:data
data segment
db "welcome to masm! ", 0
data ends
code segment
start:
; 安装 7Ch 中断例程
mov ax, cs
mov ds, ax
mov si, offset lp7c
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset lp7cend - offset lp7c
cld
rep movsb
; 设置中断向量表
mov ax, 0
mov es, ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
; 设置参数并调用中断
mov dh, 10 ; 行号 10
mov dl, 10 ; 列号 10
mov cl, 2 ; 颜色属性(绿字)
mov ax, data
mov ds, ax
mov si, 0
int 7ch ; 调用自定义中断
mov ax, 4c00h
int 21h
; 7Ch 中断例程:显示字符串
lp7c:
push ax
push es
push bx
push si
push di
push cx
; 计算显存偏移地址 (DH*160 + DL*2)
mov al, 0A0H
mul dh
mov bx, ax
mov al, 02H
mul dl
add bx, ax
mov di, cx ; 保存颜色属性
; 初始化显存段
mov ax, 0b800h
mov es, ax
; 循环写入字符
s0: mov cl, [si]
mov ch, 0
jcxz ok ; 若为 0,结束
mov al, [si]
mov cx, di
mov ah, cl
mov es:[bx], ax
add bx, 2
inc si
jmp short s0
ok: pop cx
pop di
pop si
pop bx
pop es
pop ax
iret
lp7cend:
nop
code ends
end start
程序运行后,屏幕第 10 行第 10 列处显示绿色的字符串 welcome to masm!。

int 指令对栈的影响
call 和 ret 是通过修改 IP 或 CS:IP 来实现流程控制的,而中断例程则是在中断返回之前,通过修改栈中由硬件压入的 IP 值,令 CPU 以为自己原本就应该去执行标号 s 处的代码。
模拟 loop 指令
功能需求是模拟 loop 指令,根据 CX 中的循环次数和 BX 中的位移量决定是否跳转,参数为 CX = 循环次数、BX = 跳转位移,即标号 s 到标号 se 的偏移差。loop 指令的逻辑是先执行 dec cx,然后判断 CX 是否为零,不为零则跳转到指定位置。因为中断返回应该回到 int 7ch 的下一条指令(即标号 se),所以不能直接使用 jmp,实现回跳需要通过需要修改栈中保存的返回地址。
当 int 7ch 执行时,CPU 自动将标志寄存器、CS 和 IP(指向 se)压入栈中,因此中断例程的栈中 [bp+2] 处存放的就是 se 的偏移地址。如果 CX 不为零,则将 [bp+2] 的值加上 BX 中的位移量,使其指向标号 s;如果 CX 为零,则保持栈中内容不变然后返回 se。最后使用 iret 指令,CPU 会从栈中弹出修改后的 IP 和原来的 CS,从而实现跳转到 s 或继续执行 se。
assume cs:code
code segment
start:
; 安装 7Ch 中断例程
mov ax, cs
mov ds, ax
mov si, offset lp7c
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset lp7cend - offset lp7c
cld
rep movsb
; 设置中断向量
mov ax, 0
mov es, ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
; 测试例程
mov ax, 0b800h
mov es, ax
mov di, 160*12
mov bx, offset s - offset se ; 设置从 se 到 s 的转移位移
mov cx, 80
s:
mov byte ptr es:[di], '!'
inc di
mov byte ptr es:[di], 00000010B
inc di
int 7ch ; 调用中断例程
se:
nop
mov ax, 4c00h
int 21h
; 模拟 loop 指令
lp7c:
push bp
mov bp, sp
dec cx
jcxz lpret ; 如果 cx=0,直接返回 se
add [bp+2], bx ; 修改栈中 IP 为 s 的偏移地址
lpret:
pop bp
iret
lp7cend:
nop
code ends
end start
运行后程序在屏幕第 12 行连续显示 80 个绿色的 ‘!’,每次执行 int 7ch 则 CX 减 1,只要 CX 不为零 IP 就会被修改回标号 s,从而实现循环。

考虑一个问题,7Ch 中断例程所能进行的最大转移位移取决于 BX 的取值范围是多少?因为 BX 是 16 位寄存器,作为有符号位移时,最大正向位移是 \(2^{15}-1 = 32767\) 字节,最大反向位移是 \(-32768\) 字节。因此,最大转移范围是 -32768 到 32767 字节。
模拟 jmp near ptr 指令
功能需求是模拟 jmp near ptr 指令,根据 BX 中的位移量进行近转移,参数 BX = 转移位移。jmp near ptr 指令的功能是修改 IP 的值,使其在当前 IP 的基础上加上 16 位位移。因此,中断例程只需要将栈中保存的返回地址,即 int 7ch 的下一条指令的 IP 加上 BX 中的位移即可。
assume cs:code, ds:data
data segment
db 'conversation', 0
data ends
code segment
start:
; 安装 7Ch 中断例程
mov ax, cs
mov ds, ax
mov si, offset lp7c
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset lp7cend - offset lp7c
cld
rep movsb
; 设置中断向量
mov ax, 0
mov es, ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
mov ax, data
mov ds, ax
mov si, 0
mov ax, 0b800h
mov es, ax
mov di, 12*160
s:
cmp byte ptr [si], 0
je ok
mov al, [si]
mov ah, 00000010B
mov es:[di], ax
inc si
add di, 2
mov bx, offset s - offset ok ; 设置转移位移
int 7ch ; 调用中断例程
ok:
mov ax, 4c00h
int 21h
; 模拟 loop 指令
lp7c:
push bp
mov bp, sp
add [bp+2], bx ; 修改栈中 IP 为 s 的偏移地址
pop bp
iret
lp7cend:
nop
code ends
end start
运行后程序遍历 data 段中的字符串,将其复制到屏幕第 12 行显示。每次复制一个字符后,通过 int 7ch 和 BX 中的位移跳回标号 s,直到遇到字符串结尾的 0 跳出循环。

在现代 x86 架构的保护模式和长模式下,直接修改栈中返回地址受到硬件安全机制的限制。例如,栈溢出保护(Stack Canary)会检测栈是否被意外修改;地址空间布局随机化(ASLR)使得跳转地址难以预测。在内核开发中,修改
pt_regs结构体中的ip成员是实现 内核态钩子(Kprobes) 和 系统调用拦截 的标准做法,其思想与 8086 下修改[bp+2]类似。
BIOS 与 DOS 中断例程
系统中断例程的加载
在 8086 实模式下,BIOS 和 DOS 提供了应用程序运行的底层环境。二者都以 中断例程 的形式提供服务,程序员通过 int 指令调用预置的子程序即可使用,无需关心硬件细节。这些中断例程本质上是早期操作系统的 API 调用规范,其中 BIOS 更接近硬件抽象层,DOS 则更接近现代意义上的系统调用。
| 组件 | 功能 |
|---|---|
| BIOS | 即基本输入输出系统,固化在主板 ROM 中,主要负责硬件检测、初始化以及最基础的设备控制。 |
| DOS | 在 BIOS 之上构建的操作系统,提供更贴近应用需求的功能调用。 |
BIOS 和 DOS 的中断例程由系统自动加载:
- CPU 加电启动:CS:IP 被强制初始化为 FFFF:0,执行一条跳转指令进入 BIOS 的初始化程序。
- BIOS 建立中断向量表:BIOS 将固化在 ROM 中的中断例程入口地址写入中断向量表。由于 BIOS 代码常驻 ROM,无需加载到 RAM,只需完成向量登记即可。
- 引导操作系统:BIOS 调用
int 19h进行操作系统引导,将控制权移交给 DOS。 - DOS 加载并注册例程:DOS 启动后,将其提供的中断例程加载到内存,并更新中断向量表中对应的表项,如
int 21h。
由于 FFFF:0 位于 ROM 区,ROM 是只读存储器,程序运行时无法修改其内容,因此无法改变 BIOS 的启动流程。int 19h 是 BIOS 提供的引导加载中断,用于读取磁盘引导扇区,属于硬件启动流程的一部分。DOS 是在 int 19h 执行后才被加载的,因此它不可能由 DOS 提供。
BIOS 中断例程应用
BIOS 的 int 10h 是视频服务中断,其内部包含多个子程序,通过 AH 寄存器传递子功能号。例如功能号 AH=2 对应设置光标,AH=2 对应显示字符。以下程序提供了一个样例:
assume cs:code
code segment
start:
; 设置光标
mov ah, 2 ; 功能号为 2
mov bh, 0 ; 页号为第 0 页
mov dh, 5 ; 行号:5
mov dl, 12 ; 列号:12
int 10h
; 显示字符
mov ah, 9 ; 功能号为 9
mov al, 'a' ; 要显示的字符
mov bl, 11001010b ; 颜色属性,闪烁+红底+高亮绿字
mov bh, 0 ; 页号
mov cx, 3 ; 重复显示 3 次
int 10h
mov ax, 4c00h
int 21h
code ends
end start
运行后屏幕,第 5 行第 12 列起显示 3 个闪烁的红底绿色 'a' 字符。

DOS 中断例程应用
int 21h 是 DOS 系统的中断,包含了文件管理、内存分配、进程控制等功能,常用的 mov ax, 4c00h / int 21h 就是调用其 4Ch 号子功能实现程序返回。例如以下程序的功能是在指定位置显示 "Welcome to masm!",DOS 的字符串显示功能要求字符串以 $ 结尾,且具备自动换行和上卷功能。
assume cs:code, ds:data
data segment
db 'Welcome to masm!', '$'
data ends
code segment
start:
; 设置光标位置(借助 BIOS)
mov ah, 2
mov bh, 0
mov dh, 0ch
mov dl, 40
int 10h
; 显示字符串(借助 DOS)
mov ax, data
mov ds, ax
mov dx, 0 ; DS:DX 指向字符串
mov ah, 9
int 21h
mov ax, 4c00h
int 21h
code ends
end start
运行后程序首先在 5 行 12 列设置光标,随后调用 DOS 功能显示字符串,直到遇到 $ 结束。

考虑一个在屏幕的第 2、4、6、8 行分别显示四句英文诗的程序,数据方面:定义了四个字符串 s1~s4,以 $ 结尾;s 存放 s1~s4 的偏移地址,由于是 dw,每个元素占 2 字节;row 存放对应的行号。使用 CX=4 控制循环次数,SI 用于遍历行号表,BX 用于遍历地址表。每次循环中,由于行号表元素是字节所以 inc si,由于地址表元素是字所以 add bx, 2。
assume cs:code
code segment
s1: db 'Good,better,best,','$'
s2: db 'Never let it rest,','$'
s3: db 'Till good is better,','$'
s4: db 'And better,best.','$'
s : dw offset s1, offset s2, offset s3, offset s4
row: db 2, 4, 6, 8
start:
mov ax, cs
mov ds, ax
mov bx, offset s ; BX 指向地址表
mov si, offset row ; SI 指向行号表
mov cx, 4 ; 循环 4 次
ok:
; 设置光标位置
mov bh, 0
mov dh, [si] ; DH = 当前行号
mov dl, 0 ; DL = 第 0 列
mov ah, 2
int 10h
; 显示字符串
mov dx, [bx] ; DX = 当前字符串的偏移地址
mov ah, 9
int 21h
inc si ; SI 指向下一个行号
add bx, 2 ; BX 指向下一个字符串地址
loop ok
mov ax, 4c00h
int 21h
code ends
end start
程序运行后,四句诗分别位于屏幕的第 2、4、6、8 行,左对齐显示。

在现代 x86 操作系统中,BIOS 的中断服务已被 UEFI 取代,DOS 的 int 21h 则演变成了系统调用 System Call。硬件机制方面,实模式的中断向量表变成了保护模式/长模式下的 IDT + syscall/sysenter。
参考资料
《汇编语言(第四版)》,王爽 编著,清华大学出版社

浙公网安备 33010602011771号