汇编:改写中断例程-以int9为例、更改屏幕颜色
assume cs:code
stack segment
db 128 dup(0) ; 定义栈段,大小128字节
stack ends
code segment
start:
;设置各段地址
mov ax,stack
mov ss,ax
mov sp,128 ; 栈顶指向栈段末尾(128字节栈,sp初始为128)
push cs
pop ds ; 让ds = cs(代码段和数据段同段,方便访问自定义中断例程
mov ax,0
mov es,ax ; es指向0段(中断向量表所在的段)
;安装新程序(自定义int9程序安装在0:204位置处)
mov si,offset int9 ; si = 自定义int9例程的偏移地址
mov di,204h ; di = 0段204H(新例程的存放地址)
mov cx,offset int9end - offset int9 ;cx = 新例程的字节长度
cld ; 方向标志DF=0(串操作时si/di递增)
rep movsb ; 循环复制:将cs:[si]的内容复制到es:[di],共cx字节
;将原中断地址保存在0:200单元处
push es:[9*4]
pop es:[200H]
push es:[9*4+2]
pop es:[202H]
;将自定义的int9程序所在地址CS:IP写入---向量表中,向量表触发时就转到0:204处的自定义int9处
cli ; 关中断(禁止CPU响应外部中断,防止修改向量时被打断)
mov word ptr es:[9*4],204H ; int9偏移地址 = 204H(新例程偏移)
mov word ptr es:[9*4+2],0 ; int9段地址 = 0(新例程段地址)
sti ; 开中断(恢复中断响应)
mov ax,4c00h
int 21h
;定义新中断例程
int9:
push ax
push bx
push cx
push es
in al,60H
pushf ;仅将标志位寄存器压入栈中
;调用旧中断例程
call dword ptr cs:[200h] ;调用原系统向量表保存的CS:IP地址,并执行;
;处理F1键
cmp al,3BH
jne int9ret
mov ax,0B800H
mov es,ax
mov bx,1
mov cx,2000
s:
inc byte ptr es:[bx]
add bx,2
loop s
int9ret:
pop es
pop cx
pop bx
pop ax
iret
int9end:nop
code ends
end start
关键知识点:
在 Intel 8086 CPU 中,IRET(Interrupt Return,中断返回)是一条用于从中断服务程序(ISR, Interrupt Service Routine)返回到被中断程序的指令。
一、功能说明
IRET 指令的主要作用是恢复被中断时的程序执行上下文。它会从堆栈中弹出以下内容(按顺序):
- IP(Instruction Pointer) —— 返回地址的偏移部分
- CS(Code Segment) —— 返回地址的段部分
- FLAGS(标志寄存器) —— 恢复中断前的标志状态(包括 IF、TF 等)
注意:8086 是实模式处理器,不支持保护模式下的特权级切换,因此
IRET在 8086 中只处理上述三个寄存器。
二、堆栈操作(执行 IRET 时)
假设堆栈指针为 SP,堆栈向下增长(向低地址方向),执行 IRET 相当于以下操作:
POP IP
POP CS
POP FLAGS
即:
- 从 [SP] 弹出 IP(2 字节)
- 从 [SP+2] 弹出 CS(2 字节)
- 从 [SP+4] 弹出 FLAGS(2 字节)
- 最终 SP 增加 6(因为弹出了 3 个字,每个 2 字节)
三、使用场景
IRET 必须用在中断服务例程的末尾,用于:
- 软件中断(如 INT 21h)
- 硬件中断(如 IRQ0 对应的 INT 08h)
- 异常处理(如除零异常 INT 0)
例如:
; 中断服务程序示例
my_isr:
push ax
push dx
; ... 中断处理代码 ...
pop dx
pop ax
iret ; 返回到被中断的程序
注意:在进入 ISR 时,CPU 自动将 FLAGS、CS、IP 压入堆栈;因此必须用
IRET返回,不能用RET,否则无法恢复 FLAGS,可能导致中断系统异常(如 IF 标志未恢复,导致后续中断被屏蔽)。
问题:接收用户的键盘输入,输入r,屏幕上的字符为红色;输入g,屏幕上的字符为绿色,输入b,屏幕上的字符为蓝色。
assume cs:code
code segment
start:
; ========== 1. 等待键盘输入 ==========
mov ah, 0 ; int 16h功能号0:读取键盘输入(阻塞)
int 16h ; 返回:al=按键ASCII码,ah=扫描码
; ========== 2. 识别按键并设置颜色值 ==========
mov ah, 1 ; 初始颜色值(先给蓝色位,后续通过移位调整)
cmp al, 'r' ; 判断是否按了r键(红色)
je red
cmp al, 'g' ; 判断是否按了g键(绿色)
je green
cmp al, 'b' ; 判断是否按了b键(蓝色)
je blue
jmp short sret ; 非r/g/b则直接退出
; ========== 3. 颜色值调整(通过移位定位RGB位) ==========
red: ; 红色:需要将1移到第2位(二进制 010)
shl ah, 1 ; ah=1 → 左移1次 → 2 (010)
green: ; 绿色:需要将1移到第1位(二进制 001→左移2次→100)
shl ah, 1 ; 若从red跳转:ah=2→左移1次→4 (100);若直接跳green:ah=1→左移1次→2 (010)
blue: ; 蓝色:初始值1(001),无需移位
; ========== 4. 设置屏幕颜色(显存操作) ==========
mov bx, 0B800H ; 文本模式显存起始地址
mov es, bx ; es指向显存段
mov bx, 1 ; 显存中每个字符占2字节:0位=ASCII,1位=属性(颜色)
mov cx, 2000 ; 80*25=2000个字符(全屏)
s:
; 清空原有颜色(保留高5位,低3位清0)
and byte ptr es:[bx], 11111000B
; 或运算设置新颜色(低3位:001=蓝,010=绿,100=红)
or es:[bx], ah
add bx, 2 ; 指向下一个字符的属性位
loop s ; 循环填充全屏
; ========== 5. 程序退出 ==========
sret: ; 注意:原代码此处是全角冒号,需改为半角
mov ax, 4c00h
int 21h ; DOS中断:程序退出
code ends
end start
执行 red: shl ah,1 后的完整流程解析
要明确这条指令后的执行流程,核心是理解汇编的 “顺序执行” 规则 + 代码中red/green/blue标签的 “串联逻辑”,以下分步骤拆解(结合你最终修正后的代码):
前提回顾
代码中ah初始值为1(二进制00000001),且通过cmp al,'r'跳转到red标签,说明用户按下了r键。
第一步:执行red: shl ah,1
red:
shl ah,1 ; 核心指令:ah = 1(00000001)左移1次 → ah = 2(00000010)
- 指令功能:将
ah的所有位向左移动 1 位,最低位补 0,最高位移入 CF(进位标志)。 - 执行后:
ah = 2(二进制00000010)。 - 关键规则:汇编中没有跳转 / 分支指令时,会按代码物理顺序向下执行 → 执行完
shl ah,1后,会直接跳到下一行的green:标签处。(这一步才是整个代码理解的重点)
第二步:执行green: shl ah,1
green:
shl ah,1 ; ah = 2(00000010)左移1次 → ah = 4(00000100)
- 注意:这里不是通过
je green跳转,而是 “顺序执行” 进入green标签(green本质只是代码地址标记,不是 “仅跳转才能执行”)。 - 执行后:
ah = 4(二进制00000100),对应文本模式中 “红色位(第 2 位)为 1”。 - 继续顺序执行:跳到下一行的
blue:标签处。
第三步:执行blue:及后续显存操作
blue:
mov bx,0B800H ; 显存段地址
mov es,bx
mov bx,1 ; 指向第一个字符的属性位(显存偶地址存ASCII,奇地址存颜色)
mov cx,2000 ; 80*25=2000个字符(全屏)
s:
and byte ptr es:[bx],11111000B ; 清空原有前景色(低3位)
or es:[bx],ah ; 写入红色(ah=4=100B)
add bx,2 ; 下一个字符的属性位
loop s ; 循环填充全屏
- 核心逻辑:
es指向显存段0B800H;- 循环 2000 次,每次清空显存属性位的低 3 位(原有颜色),再将
ah=4(红色)写入; bx+2是因为每个字符占 2 字节,跳过 ASCII 位直接操作属性位。
效果如下:


浙公网安备 33010602011771号