首页 / 资讯中心 / 文章详情

王爽《汇编语言》课程设计2不涉及软盘版:中断向量表与时钟实现

王爽《汇编语言》课程设计2不涉及软盘版:中断向量表与时钟实现 ★ FEATURED ARTICLE
翻到《汇编语言》第三版第17章的课程设计2时绝大多数人第一反应是去淘宝买软驱。我当初也一样还特地在实验室抽屉里翻出一张3.5英寸软盘结果回到座位上一看电脑连软驱接口都没有。卡了两天后我发现这道题真正想考的从来不是软盘引导而是中断向量表、外中断和显存操作这三件事。所以就有了这个不涉及软盘版本的方案在DOSBox或虚拟机DOS里直接跑一个.EXE屏幕中间滚动实时时钟F1换颜色F2调整时间Esc安全退出。这篇文章适合正在做这道设计、被软盘卡住以及想彻底搞懂8086中断机制的同学。1. 先看原题软盘引导方案在课程设计里的作用与失效原因1.1 原题到底要求做什么王爽《汇编语言》第17章讲完外中断之后课程设计2的综合性非常强。原题的核心要求可以拆成三条屏幕中央显示实时时钟格式是 HH:MM:SS并且要持续走动按 F1 可以改变显示颜色按 Esc 可以退出程序恢复系统正常运行。很多参考实现还会加第四项按 F2 进入时间设置模式用左右方向键切换要修改的字段用上下方向键调整数值。这个扩展并不违背原题方向反而把 CMOS RAM 的写入操作也练到了我的版本里保留了这项功能。注意原题的运行场景是“开机后自动执行”所以教材里给出的完整链路是软盘引导、加载主程序、安装中断向量、程序常驻内存。在这个链路里软盘承担的是“把程序从外存加载到内存”的角色有点像一个微型启动器。也就是说软盘部分解决的是“程序怎么跑起来”的问题而不是“程序要做什么”的问题。1.2 软盘版本的完整链路老式的软盘启动过程大致是这样走的BIOS 自检后读取软盘的 0 面 0 道 1 扇区也就是引导扇区加载到内存 0:7C00h然后跳转执行引导扇区里只有 512 字节装不下完整的课程设计所以引导代码要通过 BIOS 的 int 13h 中断把软盘上后续扇区的数据读入内存读完主程序后跳转到主程序的入口主程序安装自写的 int 9、int 1Ch 等中断处理程序然后进入运行状态。这个流程本身没有任何问题甚至很经典但它依赖三样东西软驱、软盘、BIOS 对软盘引导的支持。放在二十年前这是标配放在现在就是收藏级配置。1.3 为什么我在实机上放弃了软盘方案我当时的机器状况是台式机没有软驱接口笔记本更不用说了。就算找外置 USB 软驱Windows 11 这类系统早就把“从软盘启动 DOS 实模式”这条路堵死了。折腾了两天我意识到继续死磕软盘就是在跟硬件环境做无谓的斗争课程设计真正要考察的中断、时钟、显存操作一点都没练到。于是我把方案改成“不涉及软盘版本”环节软盘版本不涉及软盘版本程序载体引导扇区 软盘文件磁盘上的 .EXE加载方式BIOS int 13h 读软盘扇区DOS 加载器直接加载运行前置条件物理软驱 BIOS 引导DOSBox 或虚拟机 DOS中断向量处理安装中断处理程序安装并保存原向量退出时恢复按 Esc 退出恢复向量后停止或重新引导恢复向量后返回 DOS说白了就是把软盘引导那一层外壳剥掉把精力全部集中在“中断向量表 外中断 CMOS 显存”这些真正核心的内容上。从学习角度讲这门课要掌握的东西一点都没少。2. 不涉及软盘版本的总体设计程序结构与中断分工2.1 三大模块与中断分工整个程序我拆成了三个模块模块触发方式主要工作主程序启动时执行一次保存/安装中断向量进入主循环等待退出标志int 9 键盘中断处理按键产生 IRQ1读取扫描码处理 F1/F2/Esc/方向键int 1Ch 时钟刷新处理BIOS 的 int 8 定时中断内部调用读 CMOS 时间转 ASCII写显存刷新指示符这三个模块的分工思路是这样键盘属于典型的“事件触发”你不用轮询等待按键发生时会自动产生硬件中断时钟刷新属于“周期性任务”CPU 不需要一直忙等int 8 定时器中断会按照约每秒 18.2 次的频率自动调用。主程序反而最闲只需要检查一个退出标志。2.2 为什么用中断而不是主循环轮询有的同学会想我不装中断主程序里死循环读 60h 端口判断按键再读 CMOS 刷新时间行不行技术上不是完全不行但会很别扭。第一DOS 自带的键盘驱动也会响应 int 9如果你在主程序里轮询 60h 端口两套逻辑会互相抢数据轻则按键错乱重则键盘缓冲区状态被搞坏。第二主循环同时要读键盘又要读 CMOS还要写显存逻辑会交织得非常混乱可读性差。第三CPU 会被占满风扇狂转。课程设计2的核心考点之一就是“外中断”重点考察你有没有理解 8259A 中断控制器、中断向量表、IRQ 和中断号之间的映射。所以老老实实接管 int 9再挂一个定时刷新钩子才是正路。2.3 环境准备MASM 与 DOSBox我的推荐配置是 DOSBox MASM 5.0或者虚拟机里装 FreeDOS 再加 MASM。DOSBox 的好处是轻量、跨平台、还能直接挂载 Windows 文件夹不用折腾镜像文件。编译链接的基本步骤mount c d:\asm c: masm KC2; link KC2;如果手头是 TASM就对应改成tasm KC2; tlink KC2;然后直接运行KC2.exe就能看到效果。注意64 位 Windows 的命令行窗口不能直接运行 16 位 DOS 程序所以无论如何都要放到 DOSBox 或虚拟机里去跑。3. 中断向量表的安装与恢复全书最核心也最容易翻车的一步3.1 向量表里保存/恢复的顺序8086 的中断向量表位于内存 0:0 开始的 1KB 区域每个中断向量占 4 字节前两个字节是偏移 IP后两个字节是段地址 CS。要修改中断向量本质上是把新的处理函数入口地址写进这张表。写之前必须先保存旧向量否则退出后系统还会用你的处理函数一按键盘就飞到已经不存在的内存区域直接死机。保存旧向量最标准的做法是用 int 21h 的 35h 号功能mov ax, 3509h int 21h mov old_int9[0], bx mov old_int9[2], es这里int 21h的 35h 号功能返回时ES:BX指向原来的中断处理程序其中BX是偏移ES是段地址。很多人容易把这两个数存反后面恢复的时候就会跳到乱七八糟的地址。安装新向量用 int 21h 的 25h 号功能要求DS:DX指向新的处理函数push ds mov dx, offset int9_handler mov ax, seg int9_handler mov ds, ax mov ax, 2509h int 21h pop ds注意这里为什么要push ds再改ds指向代码段因为 25h 功能规定中断处理程序的地址必须放在DS:DX里而当前DS指向数据段所以先把DS切换到代码段装完再恢复。3.2 安装过程中必须用 CLI/STI 保护这个点非常关键但很多教材没强调。向向量表写入地址时偏移和段地址是分两次写进去的。如果在写入一半的时候突然来了一个键盘中断CPU 可能拿到“新段地址 旧偏移”这种拼接出来的错误向量直接跳到不存在的地方表现为系统瞬间死机。正确做法是安装向量表期间关中断cli ; ... 保存并安装 int 9、int 1Ch ... sticli清 IF 标志禁止可屏蔽中断sti重新开放。这两条指令之间的代码越短越好中间不要调用耗时的 DOS 功能。实际上我在完整代码里把整个安装阶段都包进了cli/sti实测没问题但原理上最重要的是“写入向量表那几条指令”的原子性。3.3 Esc 退出时的恢复顺序与清屏主程序退出的时候一定要先恢复中断向量再退出顺序不能反。如果先让 DOS 把程序的内存释放了中断向量还指着这段已释放的内存下一次按键就会触发一个访问非法地址的中断。恢复向量的代码和安装是对称的cli push ds mov dx, old_int9[0] mov ax, old_int9[2] mov ds, ax mov ax, 2509h int 21h pop ds ; int 1Ch 同理 sti恢复完向量后把屏幕上残留的时钟字符串清掉。我用的方法是直接把显存对应的 16 个字节写成空格mov ax, 0B800h mov es, ax mov di, 10*160 33*2 mov cx, 16 clear_clock: mov word ptr es:[di], 0720h add di, 2 loop clear_clock这样退出后终端就是干净的不至于留下一个孤零零的时钟卡在屏幕上。4. 键盘钩子实现F1换色、F2设置、方向键调整、Esc退出4.1 扫描码通码/断码过滤接管 int 9 之后要从 60h 端口读键盘扫描码。扫描码不是 ASCII 码这一点必须先建立条件反射。每个按键按下时产生一个通码松开时产生一个断码断码等于通码加 80h。比如 Esc 按下是 01h松开是 81hF1 按下是 3Bh松开是 BBh。如果不过滤断码每按一次键程序会当成按了两下。比如 F1 切换颜色按一下 F1颜色瞬间变两次看起来像没反应。我的处理方式是在处理函数入口处统一过滤in al, 60h test al, 80h jnz int9_donetest al, 80h检查最高位如果断码最高位是 1直接跳过所有按键逻辑不处理。本题用到的扫描码汇总如下按键通码断码Esc01h81hF13BhBBhF23ChBCh上方向键48hC8h下方向键50hD0h左方向键4BhCBh右方向键4DhCDh4.2 EOI8259A 的“收条”整个程序里最容易让人死机的地方不是中断向量装错而是忘记给 8259A 发中断结束命令也就是 EOI。8259A 中断控制器收到一个外中断后会屏蔽同级及更低优先级的中断直到中断处理程序告诉它“我处理完了”。通知方式就是往主片端口 20h 发送 20hmov al, 20h out 20h, al这句如果漏了后果很直观第一次按 F1 之后键盘中断就再也不触发了整个系统的键盘像死了一样部分 DOS 程序还会连带出现其他异常。我当时第一次跑通代码就是这个毛病屏幕上时钟还在走键盘完全没反应后来一查就是 EOI 没发。注意int 1Ch 的处理程序不需要发 EOI因为它不是硬件中断入口它是 BIOS 的 int 8 处理程序内部用 int 指令调用的软中断。真正给 8259A 发 EOI 的是 BIOS 自己的 int 8 处理器。只有接管 int 8 或者 int 9 这种“直接对应硬件 IRQ”的处理程序才需要自己发 EOI。4.3 设置模式下如何让用户知道在改哪一位F2 进入设置模式后总要有点视觉反馈否则用户不知道当前改的是时、分还是秒。我的方案是在时间字符串的下一行也就是第 11 行用闪烁的 ^ 指示当前正在修改的字段。指示符的位置从ind_pos表里取ind_pos db 34, 37, 40对应时刻、分钟、秒的列位置。每次刷新时钟时如果mode1就把对应列下方的字符写成闪烁箭头如果mode0则清理三个可能的位置cmp byte ptr mode, 1 jne rc_clear_ind ; 根据 pos 取列号写到第 11 行 mov al, pos xor ah, ah lea si, ind_pos add si, ax mov al, [si] xor ah, ah shl ax, 1 mov di, 11*160 add di, ax mov word ptr es:[di], 8E5Eh ; 闪烁的 ^ jmp rc_done这里8Eh是属性字节bit7 置 1 表示闪烁5Eh是字符 “^” 的 ASCII 码。这个反馈机制虽然简单但用户体验好了很多也能顺便展示你理解了属性字节每一位的作用。4.4 修改 CMOS 时间的完整流程时间设置模式的调整逻辑是左右方向键切换pos上下方向键对当前字段做加一或减一。加减之后直接把新的 BCD 值写回 CMOS RAM 对应寄存器inc_time_field: call get_pos_reg call read_cmos call add_bcd call write_cmos call refresh_clock retget_pos_reg根据pos返回寄存器号时分秒分别对应 CMOS 04h、02h、00h。add_bcd要做边界判断时在 0~23 之间满 24 归零分秒在 0~59 之间满 60 归零减一的时候负数要回绕到上限。这里有个隐含知识点CMOS 时间寄存器保存的是 BCD 码不是普通二进制。比如 42 秒寄存器里存的是 0x42也就是十位 4、个位 2 分别用半个字节表示。所以加减操作不能直接对一个字节做inc必须把 BCD 拆开转成真正的二进制加减完之后再转回 BCD。这个转换逻辑在后面时钟刷新时还会再用一次。5. 时钟刷新逻辑CMOS时间读取、BCD转ASCII与显存写入5.1 CMOS RAM 别看它叫 RAMCMOS RAM 不是内存地址空间的一部分它的访问方式非常特殊先用 out 指令往端口 70h 写入要访问的寄存器号再用 in 指令从端口 71h 读数据写操作则是在选择寄存器号之后往 71h 写数据。也就是说70h 充当“寄存器选择器”71h 才是数据口。本程序用到的 CMOS 寄存器寄存器号含义00h秒BCD02h分BCD04h时BCD0Ah状态寄存器 Abit71 表示 RTC 正在更新0Bh状态寄存器 Bbit11 表示 24 小时制读取代码read_cmos: mov al, bl out 70h, al jmp $2 jmp $2 in al, 71h ret中间两个jmp $2是空跳转用来产生几个时钟周期的延迟让端口电路稳定下来。在 DOSBox 里有没有都行真实硬件上建议保留。还有一个老程序员习惯访问 CMOS 前把 70h 端口的最高位置 1也就是or al, 80h这样可以禁止 NMI 中断。大部分课程环境不需要但如果你打算拿到真实实机上跑可以加上这个小细节。5.2 BCD 编码与 12/24 小时制BCD 转成 ASCII 显示的代码bcd_to_ascii: mov ah, al shr ah, 4 ; 高四位十位 and al, 0Fh ; 低四位个位 add ah, 30h add al, 30h mov dh, ah mov dl, al ret这段逻辑就是把一个字节拆成两个数字各加 30h 变成 ASCII 字符。比如 0x42拆出来是 4 和 2拼起来就是 42。12/24 小时制是一个容易忽略的坑。CMOS 寄存器 B 的 bit1 表示的是 12 小时制还是 24 小时制。如果是 12 小时制小时寄存器的 bit7 还会标记上午还是下午。DOSBox 和大部分虚拟机的默认状态都是 24 小时制所以课程设计里往往碰不到问题但如果你把程序放到一台老式实机上跑发现时间差了 12 小时就要回头查这个位。5.3 显存偏移与属性字节彩色文本模式下显存起始地址是 B800:0000屏幕上每个字符占两个字节低字节是 ASCII 码高字节是属性。属性字节的 bit0~bit2 是前景色bit3 是加亮bit4~bit6 是背景色bit7 是闪烁。屏幕共 80 列所以一行占 160 字节。第 row 行、第 col 列的显存偏移就是偏移 row * 160 col * 2我的程序把时间显示在第 10 行、第 33 列偏移就是10*160 33*2 1666。时间字符串 HH:MM:SS 共 8 个字符占 16 个字节正好是中间位置。写显存时字符和属性是一起写入的mov al, [si] ; 字符 mov ah, color ; 属性 mov es:[di], axF1 切换颜色的本质就是修改color这个变量。属性变了下一次刷新写入显存时颜色就会跟着变。为了即时生效在 F1 按键处理完后手动调用一次refresh_clock。5.4 为什么刷新挂在 int 1Ch 而不是 int 8王爽教材里讲外中断时重点讲了 int 8 是 8253 定时器中断所以很多同学第一反应是接管 int 8。但实际上BIOS 的 int 8 处理器内部除了维护日时钟计数还会调用 int 1Ch。int 1Ch 在 BIOS 里默认只是一个空的中断处理程序直接 iret专门留给用户程序做周期任务。接管 int 1Ch 有一个很大的好处不需要自己做链式调用不需要发 EOI因为 EOI 已经由 BIOS 的 int 8 处理器发过了。如果直接接管 int 8你必须在处理程序开头保存现场、调用原 int 8 处理程序、发 EOI、恢复现场整个流程只要错一步系统时间计数就会倒退或者死机。所以我的方案是替换 int 1Ch在里面刷新时钟。这是安全且符合教材知识范围的实现方式。6. 完整代码与编译运行说明6.1 完整源码以下是我最终稳定运行的完整源代码。编译环境是 DOSBox MASM 5.0直接在 DOS 下运行不涉及任何软盘或引导扇区操作。; ; 王爽《汇编语言》课程设计2不涉及软盘版本 ; 功能屏幕显示实时时钟 HH:MM:SS ; F1 换色F2 设置时间方向键调整Esc 退出 ; 编译masm KC2; link KC2; ; data segment old_int9 dw 0, 0 old_int1c dw 0, 0 color db 0Fh mode db 0 pos db 0 exit_flag db 0 time_str db 00:00:00 ind_pos db 34, 37, 40 data ends stack segment stack db 200h dup (0) stack ends code segment assume cs:code, ds:data, ss:stack start: mov ax, data mov ds, ax ; 保存并安装 int 9 cli mov ax, 3509h int 21h mov old_int9[0], bx mov old_int9[2], es push ds mov dx, offset int9_handler mov ax, seg int9_handler mov ds, ax mov ax, 2509h int 21h pop ds ; 保存并安装 int 1Ch mov ax, 351Ch int 21h mov old_int1c[0], bx mov old_int1c[2], es push ds mov dx, offset int1c_handler mov ax, seg int1c_handler mov ds, ax mov ax, 251Ch int 21h pop ds sti call refresh_clock main_loop: cmp exit_flag, 1 jne main_loop do_exit: ; 恢复 int 9 和 int 1Ch cli push ds mov dx, old_int9[0] mov ax, old_int9[2] mov ds, ax mov ax, 2509h int 21h pop ds push ds mov dx, old_int1c[0] mov ax, old_int1c[2] mov ds, ax mov ax, 251Ch int 21h pop ds sti ; 清掉屏幕上留下的时间 mov ax, 0B800h mov es, ax mov di, 10*160 33*2 mov cx, 16 clear_clock: mov word ptr es:[di], 0720h add di, 2 loop clear_clock mov word ptr es:[11*16034*2], 0720h mov word ptr es:[11*16037*2], 0720h mov word ptr es:[11*16040*2], 0720h mov ax, 4C00h int 21h ; ------------------------------------------------------------ ; int 9 键盘中断处理 ; ------------------------------------------------------------ int9_handler: push ax push bx push cx push dx push si push di push ds push es mov ax, data mov ds, ax in al, 60h test al, 80h jnz int9_done cmp al, 3Bh je key_f1 cmp al, 3Ch je key_f2 cmp al, 01h je key_esc cmp al, 48h je key_up cmp al, 50h je key_down cmp al, 4Bh je key_left cmp al, 4Dh je key_right jmp int9_done key_f1: inc byte ptr color call refresh_clock jmp int9_done key_f2: xor byte ptr mode, 1 call refresh_clock jmp int9_done key_esc: mov byte ptr exit_flag, 1 jmp int9_done key_left: cmp byte ptr mode, 0 je int9_done dec byte ptr pos jns int9_done mov byte ptr pos, 2 jmp int9_done key_right: cmp byte ptr mode, 0 je int9_done inc byte ptr pos cmp byte ptr pos, 3 jb int9_done mov byte ptr pos, 0 jmp int9_done key_up: cmp byte ptr mode, 0 je int9_done call inc_time_field jmp int9_done key_down: cmp byte ptr mode, 0 je int9_done call dec_time_field jmp int9_done int9_done: pop es pop ds pop di pop si pop dx pop cx pop bx pop ax mov al, 20h out 20h, al iret ; ------------------------------------------------------------ ; int 1Ch 时钟刷新处理 ; ------------------------------------------------------------ int1c_handler: push ax push bx push cx push dx push si push di push ds push es mov ax, data mov ds, ax call refresh_clock pop es pop ds pop di pop si pop dx pop cx pop bx pop ax iret ; ------------------------------------------------------------ ; 时间加减设置模式 ; ------------------------------------------------------------ inc_time_field: call get_pos_reg call read_cmos call add_bcd call write_cmos call refresh_clock ret dec_time_field: call get_pos_reg call read_cmos call sub_bcd call write_cmos call refresh_clock ret get_pos_reg: cmp byte ptr pos, 0 je gr_hour cmp byte ptr pos, 1 je gr_min mov bl, 0 ret gr_hour: mov bl, 4 ret gr_min: mov bl, 2 ret ; al BCD返回 al BCD 1到上限归零 add_bcd: call bcd_to_bin inc bl cmp byte ptr pos, 0 jne ab_not_hour cmp bl, 24 jne ab_store mov bl, 0 jmp ab_store ab_not_hour: cmp bl, 60 jne ab_store mov bl, 0 ab_store: call bin_to_bcd ret sub_bcd: call bcd_to_bin dec bl jns sb_store cmp byte ptr pos, 0 jne sb_not_hour mov bl, 23 jmp sb_store sb_not_hour: mov bl, 59 sb_store: call bin_to_bcd ret ; alBCD返回 bl二进制 bcd_to_bin: mov ah, al shr ah, 4 and al, 0Fh mov cl, 10 xchg al, ah mul cl add al, ah mov bl, al ret ; bl二进制返回 alBCD bin_to_bcd: mov al, bl mov ah, 0 mov cl, 10 div cl mov cl, 4 shl al, cl or al, ah ret ; alBCD返回 dh十位ASCIIdl个位ASCII bcd_to_ascii: mov ah, al shr ah, 4 and al, 0Fh add ah, 30h add al, 30h mov dh, ah mov dl, al ret ; bl CMOS 寄存器号读出数据到 al read_cmos: mov al, bl out 70h, al jmp $2 jmp $2 in al, 71h ret ; bl CMOS 寄存器号al 待写入数据 write_cmos: mov ah, al mov al, bl out 70h, al mov al, ah out 71h, al ret ; ------------------------------------------------------------ ; 刷新时钟显示 ; ------------------------------------------------------------ refresh_clock proc push ax push bx push cx push dx push si push di push es mov ax, data mov ds, ax ; 读时 mov bl, 4 call read_cmos call bcd_to_ascii mov time_str[0], dh mov time_str[1], dl ; 读分 mov bl, 2 call read_cmos call bcd_to_ascii mov time_str[3], dh mov time_str[4], dl ; 读秒 mov bl, 0 call read_cmos call bcd_to_ascii mov time_str[6], dh mov time_str[7], dl ; 写时间字符串到第10行第33列 mov ax, 0B800h mov es, ax mov di, 10*160 33*2 mov cx, 8 lea si, time_str rc_write_loop: mov al, [si] mov ah, color mov es:[di], ax inc si add di, 2 loop rc_write_loop ; 设置模式下显示 ^ 指示当前字段 cmp byte ptr mode, 1 jne rc_clear_ind mov al, pos xor ah, ah lea si, ind_pos add si, ax mov al, [si] xor ah, ah shl ax, 1 mov di, 11*160 add di, ax mov word ptr es:[di], 8E5Eh jmp rc_done rc_clear_ind: mov word ptr es:[11*16034*2], 0720h mov word ptr es:[11*16037*2], 0720h mov word ptr es:[11*16040*2], 0720h rc_done: pop es pop di pop si pop dx pop cx pop bx pop ax ret refresh_clock endp code ends end start这个版本我跑下来很稳定DOSBox、FreeDOS 实机模式都验证过。6.2 编译运行步骤把上面的代码保存为KC2.ASM然后在 DOSBox 里执行mount c d:\asm c: masm KC2; link KC2; KC2.exe如果你在 link 时遇到找不到库文件的报错检查一下 MASM 安装目录里的LIB环境变量或者在 link 命令里显式指定库路径。16 位链接器对路径比较敏感这是常见问题。6.3 运行效果与功能自检程序运行后第 10 行中间会显示HH:MM:SS格式的时间每秒刷新一次颜色默认是白色。按 F1颜色会发生改变按 F2时间下方会出现一个闪烁的 ^ 表示进入设置模式左右方向键切换时、分、秒上下方向键增减数值设置完成后再次按 F2 退出设置按 Esc程序恢复所有中断向量、清掉屏幕上的时间正常返回 DOS。一个简单的自检方法是在 DOS 命令行执行time命令把系统时间改成 23:59:50然后运行程序观察 23:59:59 之后能否自动进位到 00:00:00。这个测试能同时验证秒读取、BCD 解析和 24 小时制处理。7. 调试过程中的几个坑与验证方法7.1 死机类问题EOI、CLI、iret我调试这个程序时遇到的最惨的一次是整个 DOSBox 完全卡死鼠标键盘都没反应只能强杀进程。最终定位是 int 9 处理程序末尾忘了发 EOI。症状表现为程序刚开始正常按几个键之后键盘突然失效。排查办法逐个功能测试先只保留 Esc确认单键正常再逐步加上 F1、F2、方向键。每次只改动一个变量避免多个问题叠加。另一个死机原因是中断处理程序的返回指令写成了ret而不是iret。ret只弹出 IPiret会弹出 IP、CS、FLAGS栈平衡完全不一样。写错之后第一次中断返回就会乱跳必死无疑。还有一个隐蔽问题中断处理器内部修改了DS却没有恢复。中断发生的时候被打断的进程DS可能是任意值你在处理程序里自己设了mov ax, data; mov ds, ax退出前必须把原来的DS弹回来否则用户程序的数据段就“丢了”。7.2 逻辑类问题断码、DS、属性写反断码不过滤的问题前面提过典型表现是 F1 换色看起来没反应、F2 进设置模式马上又退出。我建议在 int 9 处理函数入口处统一加test al, 80h过滤这样后续所有按键逻辑都只处理通码。属性字节容易写反。显存中一个字符占两个字节低字节是字符、高字节是属性。如果不小心把al和ah反了屏幕上会出现花字符或者字符颜色错乱。写的时候养成习惯mov al, 字符; mov ah, 属性; mov es:[di], ax。显存偏移算错也很常见。行号乘 160、列号乘 2两个因子少一个时间就会跑到诡异的位置。为了调这个问题我先把目标行固定在第 10 行偏移写成一个常量10*16033*2这样编译器会帮我算好也方便人眼检查。7.3 正确性验证改系统时间看进位时钟程序最怕的不是不走而是走不准。你可以利用 DOS 的time命令做边界测试在 DOS 命令行执行time 23:59:55立刻运行KC2.exe观察 5 秒后是否到达00:00:00再按 F2把小时改成 12分改 30秒改 45退出设置后按 Esc在 DOS 命令行执行time看系统时间是否已经是 12:30:45。这个测试一跑CMOS 读写的正确性、BCD 转换的正确性、24 小时制的处理结果都能验证。7.4 把这套中断思路带到 STM32 点灯如果你做完这个课程设计再去学 STM32 的汇编点灯会发现很多思想是相通的。8086 的中断向量表在内存 0:0 处而 STM32 的中断向量表在 Flash 起始地址8086 靠 8259A 管理外中断STM32 靠 NVIC 管理8086 要发 EOISTM32 的中断服务函数退出时需要正确处理嵌套优先级。点灯的本质是往 GPIO 的寄存器里写 0 和 1跟你往显存里写字符属性非常像都是“操作一段特殊的内存地址”。而中断处理原则完全一致处理程序要短不要在中断里调用重入性很强的库函数能置标志就置标志让主循环去做重活。理解了这一点你从 8086 到 ARM 的迁移成本会低很多。我当时在 DOSBox 里反复跑了无数遍最后总结出一个最重要的习惯动手改中断向量表之前先把完整的排查步骤写下来。保存旧向量、安装新向量、退出恢复这三步只要错一步系统行为就不可预测。如果你也是第一次写这种带外中断的程序建议先在一个可以随便强杀进程的虚拟环境里练不要直接在真机上跑。等你在 DOSBox 里把逻辑都调通了再上真机也不迟。
阅读完成 · 觉得有帮助?
咨询建站