08_中断程序设计
基于 x86 实模式汇编。
一、 端口与 I/O 指令底层交互机制
CPU 无法直接控制外部硬件,必须通过端口 (Port) 与外设进行数据交换。端口是外设控制器(如键盘控制器、中断控制器)内部的寄存器,在独立的 I/O 地址空间中统一编址。
1. IN 指令与 OUT 指令规范
-
IN(Input): 从端口读取数据到 CPU。-
语法:
IN AL, 端口号或IN AX, 端口号。 -
示例:
in al, 60h。从 60h 端口(键盘数据端口)读取当前按下的键码存入 AL 寄存器。
-
-
OUT(Output): 从 CPU 发送数据到端口。-
语法:
OUT 端口号, AL或OUT 端口号, AX。 -
示例:
mov al, 20h配合out 20h, al。向 20h 端口(8259A 中断控制器的命令端口)发送 20h 这个特殊数据(End Of Interrupt, EOI 信号)。
-
二、 中断向量机制与代码段寻址策略
中断向量表固定在物理内存 00000H 处,占用 1024 字节。中断号 n 的向量(即处理程序的偏移地址 IP 和段地址 CS)保存在 0:[n*4] 的双字内存单元中。
1. old_8h 必须定义在 Code 段内
在编写硬件中断(如 INT 8H 时钟中断)接管程序时,原中断向量通常保存在变量 old_8h 中。该变量必须定义在代码段 (CS) 内,而不能定义在数据段 (DS) 内。
-
原因: 硬件中断是异步发生的,发生时 CPU 可能正在执行任何进程的代码,此时的
DS寄存器指向的是被中断进程的数据段,而非中断处理程序的数据段。 -
寻址方式: 在中断处理函数内部,唯一绝对可靠的段寄存器是
CS。将变量放在代码段,使用cs:old_8h强制段前缀访问,能确保无论何时发生中断,都能精准找到原中断向量以实现恢复或中断衔接。
2. 自定义软件中断 (如 INT 80H)
通过直接修改中断向量表,可以将自定义函数绑定到空闲中断号上:
; 截取并替换 INT 80H
mov ax, 0
mov es, ax
mov bx, 80h*4
; 1. 保存原向量
mov ax, es:[bx]
mov cs:old_80h[0], ax ; 存入CS段内的变量
mov dx, es:[bx+2]
mov cs:old_80h[2], dx
; 2. 写入新向量
mov word ptr es:[bx], offset int_80h
mov word ptr es:[bx+2], cs ; 覆盖CS地址
三、 硬件中断接管与常驻内存 (TSR)
修改硬件中断(如 INT 8 时钟,INT 9 键盘)向量时,必须遵循严格的原子操作与硬件响应规范。
1. 中断修改的安全规范 (CLI / STI)
修改中断向量表时分为修改偏移量和修改段地址两步。如果在两步之间触发了该中断,CPU 将跳转到极其危险的错误地址。因此,必须使用 CLI 清除 IF 标志位(屏蔽硬件中断),修改完成后再 STI 恢复。
2. EOI 与 中断衔接 (Interrupt Chaining)
-
直接返回 (发 EOI): 接管硬件中断后,若不调用原中断程序,必须手动向硬件发送 EOI 信号,否则中断控制器将被阻塞,不再发送后续同级或低级中断。
-
中断衔接 (推荐): 直接跳转到系统原中断处理程序,由原程序负责发送 EOI 并完成系统级更新(如更新系统时间)。
3. 内存驻留退出 (INT 21H AH=31H)
如某锁血外挂程序。接管时钟中断并在后台周期性修改游戏内存数据,主程序不能用 4CH 退出,必须用 31H 驻留内存。(DOS是单线程的,不能同时运行外挂程序和游戏程序,要防止后续运行的游戏程序覆盖外挂程序原本占用的内存)。
-
AH=62H, INT 21H:获取当前程序的 PSP(程序段前缀)段地址,存储于BX。 -
AH=31H, INT 21H:保留指定大小的内存(以DX为节,1 节=16字节)并退出。计算方法需包含代码段长度 + 100H(PSP大小),加上 0Fh 后右移 4 位,进行按节向上取整。
驻留内存的程序(外挂)在后台运行时,如何安全、准确地修改目标进程(如游戏)的内存?
-
PSP 段定位: 通过
INT 21H (AH=62H)可以获取当前正在运行的任意进程的 PSP(程序段前缀)段地址,该地址是进程数据区的基址。 -
特征码校验: 为了防止在非目标进程执行时引发内存越界或系统崩溃,必须先通过偏移地址读取特定内存(如
ds:[985h]),比对特征码(即二进制文件中已知的特定字节)。 -
动态修改: 只有特征码匹配,确认当前是目标游戏进程时,才通过特定的偏移量(如
ds:[1809h])去锁定或修改内存数据(如生命值等)。
代码实现:
mov ah, 62h
int 21h ; BX = 当前活动进程的 PSP 段址
mov ds, bx
cmp byte ptr ds:[985h], 48h ; 校验特征码,确认是否为目标游戏
jne skip
game_is_running:
mov byte ptr ds:[1809h], 0Bh ; 确认无误,锁定生命值
4. 时钟中断的典型应用:延时控制
由于 INT 8H 时钟中断每秒固定触发约 18.2 次,程序可以通过接管该中断来实现不依赖 CPU 频率的绝对精准延时。
-
实现机制: 在数据段或代码段定义一个全局变量(如
ticks)。在主程序中将其设置为目标延迟次数(如延迟 1 秒设为 18)。 -
中断处理逻辑: 自定义的中断函数
int_8h每次执行时,检查ticks,若大于 0 则将其减 1。 -
主程序阻塞等待: 主程序通过
cmp [ticks], 0并在不为 0 时循环等待(jne wait_a_while),从而实现精准的异步延时,且不影响 CPU 响应其他底层硬件中断。
代码示例:
delay_1s:
mov [ticks], 18 ; 设置计数器为 18 次 (~1秒)
wait_a_while:
cmp [ticks], 0 ; 每次中断发生后,ticks 会被 int_8h 减 1
jne wait_a_while ; 只要不为 0 就死循环等待
ret
四、 键盘底层中断设计 (INT 9H) 详解
键盘通过 60h 端口直接向 CPU 传送硬件扫描码,每次按键状态变化都会触发 INT 9H。
1. 扫描码的按键状态编码 (Make/Break Code)
键盘每次按键会产生两个编码:按下时产生通码 (Make Code),松开时产生断码 (Break Code)。
-
按下 (最高位为 0): 例如
A键按下时为1Eh(即二进制0001 1110)。 -
抬起 (最高位为 1): 断码 = 通码 + 80h。例如
A键抬起时为9Eh(即二进制1001 1110)。
2. 键盘编码上限与前缀扩充码
由于 1 字节 8 位最多表示 256 个状态,扣除一半用于断码,原生扫描码最多只能表示 128 个物理按键 (2^8 / 2 = 128)。
随着键盘按键增多(如引入多媒体键、右侧 Alt/Ctrl 等),128 个编码不够用。为此,键盘硬件允许某些键共享基础编码,但使用前缀扩充码 (Prefix Code)进行区分。
-
扩充码占多个字节,最常见的前缀是
E0h和E1h。 -
处理逻辑: 当
IN AL, 60h读到E0h时,表示这是一个扩充键序列的开始,下一个触发INT 9传来的才是真实键码。例如,右 Alt 键按下会触发两次中断,先后发送E0h和38h(左 Alt 仅发送38h)。
3. 扫描码的可视化输出策略
在接管 INT 9H 后,获取的 AL 寄存器内容是原始的二进制扫描码,无法直接通过 INT 21H 打印,必须手动将其拆分为高 4 位和低 4 位,映射到 ASCII 字符集。
-
XLAT 查表指令: 汇编中使用
XLAT指令(查表翻译)是最高效的做法。预先定义好 16 进制字符表hex_tbl db '0123456789ABCDEF'。 -
位运算拆解:
-
将
AL右移 4 位(shr al, 4)得到高位,作为偏移量加到BX(基址表),利用XLAT转换为对应的 ASCII 码。 -
将原
AL与0Fh做按位与(and al, 0Fh)获取低位,再次查表。
-
-
底层视频中断调用: 拼接好字符后,通过
INT 10H (AH=0EH)将其输出到屏幕。
五、 单步调试中断 (INT 1H) 机制分析
INT 1H 是调试器(如 Debug、OllyDbg 等)实现代码单步步过的基石,由硬件的 TF (Trap Flag,陷阱标志位) 直接驱动。
1. 触发逻辑与插入模式
当标志寄存器 FL 中的 TF = 1 时,CPU 进入单步模式。
-
插入模式机制: 若在某条指令执行前
TF=1,那么在该条指令执行后,CPU 硬件会强制自动插入一条INT 1中断调用。 -
防止递归防线: CPU 进入
INT 1H处理程序内部时,硬件会自动将 IF 和 TF 标志位清零。这非常关键,否则INT 1H处理程序内的第一条指令执行后又会触发INT 1H,导致无限递归堆栈溢出。中断结束执行IRET弹出原标志寄存器时,自动恢复 TF=1 状态。
2. 调试器的接管套路
调试器会修改中断向量表 0:[4],将 INT 1 指向调试器内部的控制函数。
-
开启单步: 由于无法直接修改 TF 位,必须通过堆栈周转修改。
-
应用场景(如指令动态加解密): 通过
INT 1H在每条指令执行后触发中断,中断处理函数通过提取堆栈中保存的CS:IP(指向下一条即将执行的指令),去动态修改下一条指令的机器码,使其恢复成正常指令并执行,执行完后再将其破坏。这是一种极强的软件保护(反汇编/反调试)手段。