汇编:中断及处理

中断(Interrupt)是 CPU 暂停当前执行流程、转而处理紧急 / 特殊事件(如硬件请求、软件异常、系统调用)的机制,是操作系统和硬件交互的核心。以下从中断的核心概念、中断处理流程、汇编实现示例三方面详细讲解。


一、中断的核心概念

1. 中断的分类

类型触发方式典型场景
硬件中断外设(如键盘、硬盘)触发键盘按键、鼠标点击、定时器溢出
软件中断指令主动触发(如INT n系统调用(如 DOS 的INT 21H)、程序异常
异常(陷阱)指令执行错误除 0、内存越界、缺页

2. 中断向量表(IVT)

x86 架构中,中断处理的核心是中断向量表(IVT):

  • 位于内存低地址(0x00000 ~ 0x003FF),共 256 个中断向量(0~255);
  • 每个向量占 4 字节:2字节偏移地址(IP) + 2字节段地址(CS),指向对应中断处理程序的入口;
  • 例如:中断号0x21对应 IVT 中0x21*4=0x84地址开始的 4 字节,存储 DOS 系统调用处理程序的地址。

3. 中断处理的核心步骤

  1. 触发中断:硬件 / 软件向 CPU 发送中断请求(如执行INT 0x21);
  2. 保护现场:CPU 自动压栈FLAGSCSIP(若有错误码则额外压栈);
  3. 关中断:CPU 自动清除FLAGS中的 IF 位(避免嵌套中断干扰);
  4. 查找处理程序:根据中断号计算 IVT 地址,加载CS:IP指向中断处理程序;
  5. 执行处理程序:处理中断逻辑(需手动保护通用寄存器);
  6. 恢复现场:手动恢复寄存器,执行IRET指令(自动弹出IPCSFLAGS);
  7. 恢复执行:回到原程序中断处继续执行。

二、示例:系统中的0号中断

assume cs:code
code segment
start:
	mov ax,8
	mov bh,0
	div bh
code ends
end start

用DOSBOX-X运行如下:

一、先明确:触发的是哪类中断?

div bh8 位除法指令

  • 被除数默认在AX(16 位),除数是BH(8 位),运算逻辑为 AX ÷ BH,结果商存AL、余数存AH
  • 你代码中BH=0,除数为 0 → 触发除法溢出中断(Divide Error),对应 x86 的中断类型号 0(Intel 官方定义,中断类型 0 是除法错误 / 溢出)。

二、中断向量表(IVT):中断类型号→中断服务程序入口的映射

x86 实模式下,内存最低地址0000:0000~0000:03FF(共 1KB)是中断向量表(IVT),每个中断类型号对应一个 “4 字节入口地址”(2 字节 IP + 2 字节 CS),规则:

中断类型号 n → 向量表地址 = 0000: n×4

入口地址格式:[0000:n×4] = IP[0000:n×4+2] = CS

对于中断类型 0

向量表地址 = 0×4 = 0000:0000 → 需读取0000:0000(IP)和0000:0002(CS),这两个值决定了中断服务程序的入口。

三、为什么最终 CS=F000H、IP=CA60H?

DOSBox-X 模拟的是IBM PC/XT/AT 兼容的实模式环境,而 PC 兼容机的 BIOS(基本输入输出系统)将核心中断服务程序存放在F000:0000~FFFF:FFFF(64KB BIOS ROM 区)。具体流程:

步骤 1:BIOS 初始化中断向量表

开机时,BIOS 会初始化 IVT,将中断类型 0 的入口地址 设置为 BIOS 中 “除法错误处理程序” 的地址:

  • 0000:0000写入CA60H(IP);
  • 0000:0002写入F000H(CS)。

这是 PC 兼容机的标准 BIOS 布局(不同 BIOS 版本可能略有差异,但F000H段是 BIOS ROM 的核心段,CA60H是除法错误处理程序在该段的偏移)。

步骤 2:CPU 响应中断的硬件行为

div bh触发除法错误时,CPU 自动执行以下操作(实模式):

  1. 压栈 FLAGS、CS、IP(保护现场);
  2. 从 IVT 的0000:0000读取 IP(CA60H),从0000:0002读取 CS(F000H);
  3. 将 CS 设为F000H,IP 设为CA60H,跳转到该地址执行中断服务程序。

因此你在 DOSBox-X 中看到的CS=F000H、IP=CA60H,正是 CPU 从 IVT 中读取的、BIOS 预设的 “除法错误中断服务程序” 入口地址。

三、编制中断处理程序

问题:自行编写0号中断处理程序

assume cs:code
code segment
start:
	; 源地址:cs:offset do0(当前代码段的do0标签处)
	mov ax,cs
	mov ds,ax     		; ds = cs(源段地址)
	mov si,offset do0	; si = do0的偏移地址(源偏移)
	
	; 目标地址:0:200H
	mov ax,0
	mov es,ax		; es = 0(目标段地址)
	mov di,200H		; di = 200H(目标偏移)
	
	; 复制长度:do0end - do0(中断处理程序的字节数)
	mov cx,offset do0end-offset do0
	; 将方向标志DF清零,使字符串操作(如movsb)按地址递增方向执行
    ; 即:每次操作后SI和DI自动增加(+1,取决于操作数大小,这里是字节操作所以+1)
	cld			;置DF=0,SI和DI自动+1,并将DS:[SI]复制到ES:[DI]
	rep movsb
	
	;将自定义的CS:0000,IP=0200分别写入0号中断地址0000:0000和0000:0002处
	mov ax,0
	mov es,ax
	mov word ptr es:[0*4],200H
	mov word ptr es:[0*4+2],0
	
	mov ax,4c00h
	int 21h
	
do0:jmp short do0start
	db 'overflow'
do0start:
	;这三行代码的任务就是定位到字符串‘overflow’的首地址。
	mov ax,cs
	mov ds,ax
	mov si,202H
	
	;这一段就是将‘overflow’在屏幕中间显示出来
	mov ax,0B800H
	mov es,ax
	mov di,12*160+36*2
	mov cx,9
s:	mov al,[si]
	mov es:[di],al
	inc si
	add di,2
	loop s 
	
	mov ax,4c00h
	int 21H
do0end:nop
code ends
end start

关键点说明:

1.cld指令:在每次调用 rep movsb前都应确保方向标志正确。CLD表示正向(从低地址到高地址),STD表示反向。
2.rep movsb执行过程
  • 检查 CX 是否为 0,为 0 则结束
  • 执行 movsb:将 DS:[SI]的一个字节复制到 ES:[DI]

  • SI 和 DI 自动 +1(因为 DF=0)

  • CX 减 1

  • 重复直到 CX=0
3.offset偏移地址的说明

在 8086 汇编中,offset 标号 计算的是该标号相对于其所在段的段起始地址的偏移地址(段内偏移),具体到这段代码中:

核心结论

offset do0 的偏移地址是相对于 code 段的段起始地址(code 段的基地址)计算的,与 CS 寄存器的值无关(编译 / 汇编阶段确定,而非运行时)。

详细解释

3.1. offset 是汇编期伪操作,而非运行时指令

offset 由汇编器(如 MASM/TASM)在编译阶段计算,而非 CPU 运行时计算:

  • 汇编器会为每个段分配一个 “虚拟的段起始地址”(默认从 0 开始),然后计算标号相对于该起始地址的偏移。
  • 例如:
    • startcode 段的第一个标号,offset start = 0
    • startdo0 之间的指令会占用字节,汇编器会累加这些字节数,得到 do0 相对于 code 段起始的偏移。

3.2. 结合代码的实际场景

这段代码中,do0 位于 code 段内,因此:

mov si,offset do0  ; si = do0在code段内的偏移(汇编期确定)

而后续通过 mov ax,cs; mov ds,axDS 指向 CS(即 code 段的实际段地址),此时 DS:SI 就精准指向了 do0 在内存中的实际地址。

3.3. 关键区分:段内偏移 vs 物理地址

  • offset do0:仅表示段内偏移(16 位),与段地址无关;
  • 物理地址 = 段地址 × 16 + 段内偏移(如 CS×16 + offset do0do0 的实际物理地址)。
4.修改 0 号中断向量表
mov ax,0        
mov es,ax       ; es = 0(中断向量表所在段)
mov word ptr es:[0*4],200H  ; 0:0 写入偏移地址200H
mov word ptr es:[0*4+2],0   ; 0:2 写入段地址0

中断向量表中,0 号中断的自定义入口地址存放在 0:0(偏移)和 0:2(段地址),这里将其指向自定义的中断处理程序0:200H,如下图所示:

5.为什么是 202H?

自定义中断处理程序do0被复制到0:200H,而do0的第一条指令是:

do0:jmp short do0start  ; jmp short 是2字节指令(偏移200H~201H)
db 'overflow'           ; 字符串从偏移202H开始

因此SI=202H恰好指向字符串overflow的第一个字符(o),后续可通过DS:SI逐个读取字符。

这段代码的任务就是定位到‘overflow’字符串的首地址,以便后续使用每个字符。

6.为什么是 36 列(36×2)?

要让overflow(9 个字符)在 80 列的屏幕中水平居中,计算逻辑如下:

  • 屏幕总列数:80 列;
  • 字符串长度:9 个字符;
  • 居中列起始位置 = (80 - 9) ÷ 2 = 35.5 → 取整为 36 列(视觉上更居中)。

对应显存偏移:

  • 第 36 列的偏移 = 36 列 × 2 字节 / 字符 = 72(即36*2,十六进制为48H)。
  • di = 12*160 + 36*2
    

    四、中断程序的运行

将上面编写的中断程序使用masm命令编译、联接后生成exe文件,直接运行exe文件。

然后,进入debug模式,用a命令编写三行测试代码:

mov ax,8
mov bh,0
div bh

再使用g命令直接无中断运行,‘overflow’就会出现在屏幕中间了。

posted @ 2025-12-09 22:08  chenlight  阅读(6)  评论(0)    收藏  举报  来源