汇编语言:中断例程的调用

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 号中断主动触发了。
image

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)。
image

转换字符串为大写

功能需求是将以 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'。
image

显示字符串

功能需求是显示以 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!。
image

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,从而实现循环。
image

考虑一个问题,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 跳出循环。
image

在现代 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 的中断例程由系统自动加载:

  1. CPU 加电启动:CS:IP 被强制初始化为 FFFF:0,执行一条跳转指令进入 BIOS 的初始化程序。
  2. BIOS 建立中断向量表:BIOS 将固化在 ROM 中的中断例程入口地址写入中断向量表。由于 BIOS 代码常驻 ROM,无需加载到 RAM,只需完成向量登记即可。
  3. 引导操作系统:BIOS 调用 int 19h 进行操作系统引导,将控制权移交给 DOS。
  4. 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' 字符。
image

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 功能显示字符串,直到遇到 $ 结束。
image

考虑一个在屏幕的第 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 行,左对齐显示。
image

在现代 x86 操作系统中,BIOS 的中断服务已被 UEFI 取代,DOS 的 int 21h 则演变成了系统调用 System Call。硬件机制方面,实模式的中断向量表变成了保护模式/长模式下的 IDT + syscall/sysenter。

参考资料

《汇编语言(第四版)》,王爽 编著,清华大学出版社

posted @ 2026-08-21 00:05  乌漆WhiteMoon  阅读(14)  评论(0)    收藏  举报