资讯详情 Debug调试汇编闰年程序:寄存器残留引发循环失控的完整排查
📅 2026/10/15 15:05:50
简介面向北京交通大学汇编与接口课程的Debug调试实验资料专为正在学习汇编语言与微机接口的学生设计适合需要借助调试工具理解程序执行过程的初学者。资源包内共1个docx文件大小约78.94MB内容以leapYear.exe闰年判断程序为载体完整记录了从编译、链接、运行到Debug调试的完整过程目前已有520人浏览学习。实验报告先对代码整体逻辑进行了梳理讲解了闰年判断条件再通过单步执行、设置断点等方式逐句分析汇编指令演示输入闰年与非闰年时寄存器、内存状态的变化同时介绍D、T、C、P等常用调试指令的实际用法。针对实验中发现的条件判断错误报告还给出了问题定位与源代码修改思路并在重新链接编译后验证运行结果。这份资料既能辅助学生完成同类上机实验也可作为汇编调试技能梳理与排错方法学习的参考。1. 用 Debug 调试汇编闰年程序把“看起来正常”的运行结果打回原形某高校汇编与接口课的机试周总有人卡在同一个 Debug 调试实验上教材给的闰年判断程序直接运行是对的但只要用 Debug 的 G 命令跑一遍不管输入 2020 还是 2026输出的都是 “This is a leap year!”。这个现象乍看像玄学本质是一个典型的寄存器高位残留问题。本篇笔记把这份实验资源按“先读代码、再编译链接、然后逐行调试、最后修错验证”的顺序完整拆开适合正在补做汇编上机实验或者想搞明白 Debug 常用命令R、U、T、P、D、G、E怎么配合使用的人。资源包里包含可编译的源码、完整调试记录和修改前/后的对照说明照着复现一遍比干背指令效果强得多。2. 先把程序读透再动手leapYear 的数据流与三个关键子程序2.1 整体执行流输入 → DATACATE 转换 → IFYEARS 判断 → CF 分支程序的主线并不复杂先显示提示串等待键盘输入年份把年份字符串转成二进制数再调用闰年判断子程序最后根据进位标志 CF 决定打印哪条结果。用汇编的视角看关键不在“闰年算法”本身而在字符串怎么变成数、标志位怎么回传、寄存器现场怎么保护。主流程里藏着两个 INT 21H 的服务调用AH09H 用于输出以$结尾的字符串AH0AH 用于接收带缓冲的键盘输入。输入完成后[BUF1]里存的是用户实际键入的字符个数真正的字符从[BUF2]开始存放。程序先取这个长度到 CL再把缓冲区首地址送给 SI/DICALL DATACATE 完成字符到数字的转换再 CALL IFYEARS 判断最后用 JC A1 查看 CF。整体逻辑一环扣一环任何一个寄存器传输出问题结果都会偏。2.2 数据段布局BUF 输入缓冲与 W 存储字的地址分配逻辑数据段是整段程序的内存地基先看清楚每个变量长什么样DATA SEGMENT INFON DB 0DH,0AH,Please input a year: $ Y DB 0DH,0AH,This is a leap year! $ N DB 0DH,0AH,This is not a leap year! $ W DW 0 BUF DB 8 DB ? DB 8 DUP(?) DATA ENDS0DH,0AH是回车换行的 ASCII 码放在提示串前面保证输出文字之前先换行。字符串末尾的$不能丢AH09H 输出字符串时遇到$才停止省略它会把后面一大片内存当成字符打印出来。BUF采用的是 DOS 10H 号功能的输入缓冲区结构第一个字节填最大允许长度这里填 8留出 4 位年份、回车和余量第二个字节由系统回填实际输入长度第三个字节起才是字符数据所以输入字符的真正起点是BUF2。W是 16 位字变量DATACATE 结束之后年份的十六进制值就存在这里。2.3 DATACATE 子程序减 30H 与乘十累加的转换原理年份是字符串CPU 不认识必须先转成数值。DATACATE 的做法是经典“乘十累加”每一轮取一个字符减掉30H得到数字再把之前的结果乘 10 加上新数字。DATACATE PROC NEAR LEA SI, BUF2 MOV BX, 0 X3: MOV AL, [SI] SUB AL, 30H ; ASCII 转数字0 的 ASCII 是 30H MOV AH, 0 ; 把 AX 高位置零保证后续乘法正确 XCHG AX, BX MOV DX, 10 MUL DX ; AX AX * 10 XCHG AX, BX ADD BX, AX INC SI LOOP X3 MOV W, BX RET DATACATE ENDPSUB AL,30H把字符232H变成数字 2MOV AH,0之后 AX 才是干净的 16 位数字。XCHG AX,BX的目的是把上一轮累计值换到 AX 参与乘法乘完再换回来把新数字加进 BX。走完四次循环2020 就变成 07E4H。这里值得留意的是LOOP X3LOOP 的循环条件是整个 CX 不为 0而主程序只给 CL 赋了值CH 是残留状态。这就是本实验埋下的最大雷点后面 Debug 段会专门复盘。2.4 IFYEARS 子程序与 4、100、400 取余的判定顺序与 CF 返回约定闰年的判定规则是能被 4 整除、但不能被 100 整除或者能被 400 整除。IFYEARS 用三段除法实现并用 CF 作为返回值CF1 表示闰年CF0 表示不是。IFYEARS PROC NEAR PUSH BX PUSH CX PUSH DX MOV AX, W MOV CX, AX ; 保存年份副本 MOV DX, 0 ; DIV 之前必须清空 DX MOV BX, 4 DIV BX ; AX 存商DX 存余数 CMP DX, 0 JNZ LAB1 ; 不能被 4 整除直接返回“不是” MOV AX, CX MOV BX, 100 DIV BX CMP DX, 0 JNZ LAB2 ; 能被 4、不能被 100 整除是闰年 MOV AX, CX MOV BX, 400 DIV BX CMP DX, 0 JZ LAB2 ; 能被 400 整除也是闰年 LAB1: CLC ; CF 清零 JMP LAB3 LAB2: STC ; CF 置 1 LAB3: POP DX POP CX POP BX RET IFYEARS ENDP开头三个 PUSH 是保护现场主程序的 BX、CX、DX 还要用子程序不能随意改掉。DIV BX的被除数是 DX:AX 组成的 32 位数所以每次除法前MOV DX,0是必须的漏掉这条DX 里的残留值会直接参与除法轻则算错重则除法溢出。LAB1、LAB2、LAB3 三个标号构成一个典型的“统一出口”结构不管哪条分支最后都流到 LAB3 做 POP 和 RET。注意 LAB2 里没有 JMP它写完后直接落入 LAB3这是故意让两条分支共享恢复代码。3. 编译链接到可执行文件masm5 环境下生成 leapYear.exe 的关键动作3.1 环境准备masm5 目录结构与路径要求动手调试之前先把环境摆平。资源里使用的是 MASM 5.0整个工具包通常是一个文件夹里面放着 MASM.EXE、LINK.EXE、DEBUG.EXE。我一般把 masm5 文件夹直接放在 C 盘根目录路径越短越好避免长目录名和空格给 16 位工具带来奇怪的问题。如果机器上没有纯 DOS 环境用虚拟机装一个精简 DOS 镜像或者用支持 16 位程序的环境跑 MASM 都行。把 leapYear.asm 源文件放进 masm5 同一目录后面操作就不用反复切路径。打开命令行进入 C:\MASM5先确认源文件在眼前cd C:\MASM5 dir leapYear.asm看到文件存在再继续。这一步容易翻车的点在于目录搞错、文件名大小写不一致、源文件没有保存成纯文本格式都会让下一步 MASM 直接报“文件找不到”。3.2 汇编命令 masm编译器的三连问和分号的作用MASM 5.0 是问答式工具执行后它会依次询问目标文件名、列表文件名、交叉引用文件名。如果不想一次次回车直接在你给的源文件名后面加一个分号它就会用默认名一路跳过。C:\MASM5 masm leapYear; Microsoft (R) Macro Assembler Version 5.00 Copyright (C) Microsoft Corp 1981-1988. All rights reserved. Object filename [leapYear.OBJ]: Source listing [NUL.LST]: Cross-reference [NUL.CRF]: 0 Warnings 0 Errors看到 “0 Warnings, 0 Errors” 才算过。分号的作用是代替三个回车让每项提示都采用方括号里的默认值。编译报错时MASM 会给出行号和错误码比如 A2006 表示未定义符号A2009 表示符号冲突这时候不要急着改先回头检查数据段变量名和代码里的引用是否一致。3.3 链接命令 link从 OBJ 到 EXE 的收尾工作编译产出的是 OBJ 目标文件还不能直接运行必须经过链接器把段、变量、入口地址整理进可执行文件C:\MASM5 link leapYear;同样用分号跳过剩余询问。链接成功后masm5 目录下会多出 leapYear.EXE。如果代码里定义了 STACK SEGMENT链接器不会报栈缺失一旦你把堆栈段名拼错或者漏掉LINK 会提示L4021 no stack segment。这个警告在简单实验里不影响运行但最好不要带着它往下走不然中断调用时的栈容易出问题。3.4 生成的文件清单OBJ、LST、CRF、MAP、EXE 各自用途一次编译链接下来工作目录会多出一堆文件很多初学者只认识 EXE其他文件全当成垃圾。这里整理一下各自用途文件生成阶段用途leapYear.OBJMASM 汇编后目标文件LINK 的输入不参与运行leapYear.LSTMASM 汇编后列表文件源码与机器码逐行对照Debug 查偏移地址很关键leapYear.CRFMASM 汇编后交叉引用文件记录符号在哪定义、在哪引用leapYear.MAPLINK 链接后链接映像列出各段地址和入口位置leapYear.EXELINK 链接后可执行文件最终调试对象个别环境还会额外生成 .REF 辅助文件一般是交叉引用功能的附带输出不影响实验。调试阶段最值得看的是 .LST它把每一条汇编语句的段内偏移都标了出来和 Debug 的 U 命令输出对照使用能快速定位当前执行到哪一句。3.5 加载到 Debug两种启动方式与寄存器状态的“假象”EXE 生成后用 Debug 加载它有两个入口。最直接的是命令行带文件名C:\MASM5 debug leapYear.exe进去后用 R 命令能看到寄存器初值这时注意两条IP 指向 0000也就是 EXE 代码入口CX 的值是文件相关信息不是年份数据。很多人在这里被带偏以为 CX 就该像“2020”一样大后面 Debug 出问题就往年份上想实际是寄存器初始状态在作怪。也可以先启动 debug再用 N 和 L 加载文件-n leapYear.exe -l -r两种方式效果一样。DEBUG.EXE 是 DOS 自带的调试器16 位环境下直接可用。加载完先别急着跑下一步要结合 R、U、T、P 这些指令一步步看现场。4. Debug 逐行追踪R、U、T、P、D、E、G 组合拳拆解闰年判断现场4.1 R 与 U进场先看寄存器初值和反汇编清单Debug 加载 EXE 后第一件事是用 R 查看寄存器。资源里的调试记录显示加载后 IP 在 0000代码段地址是一个动态段值例如 0C59CS:IP 组合起来才是指令地址。-r AX0000 BX0000 CX0C59 DX0000 SP0000 BP0000 SI0000 DI0000 DS0C59 ES0C53 SS0C4B CS0C59 IP0000R 显示的是当前所有寄存器和标志位的值。注意这里 CX 并不是 0这是 EXE 加载带来的初始状态。接着用 U 反汇编看当前要被执行的机器码对应什么汇编语句-u 0C59:0000 B8430C MOV AX,0C43 0C59:0003 8ED8 MOV DS,AX 0C59:0005 BA0000 MOV DX,0000U 命令的格式是“段地址:偏移 机器码 汇编指令”。用 .LST 文件对照来看能确认编译出来的地址和源程序是一一对应的。这一步能解决很多“我明明改对了但程序行为不变”的问题如果 U 出来的内容还是老样子说明链接生成的 EXE 没更新。4.2 T 与 P进入子程序与跳过中断的选择T 和 P 都用于单步执行区别在于怎么处理 CALL 和 INT。T 会踏进子程序内部一条一条地执行子程序里的指令P 则把 CALL 或 INT 当做一个整体直接执行到返回点。这个区别在资源里被反复验证过用 T 跟踪 INT 21H会直接钻进 DOS 中断服务程序输出一串看不懂的系统代码想再跳出来非常痛苦换成 P 就干净利落。实操中我的套路是在 CALL DATACATE 之前用 T进去观察转换过程在 INT 21H 前面用 P跳过整个中断调用。这样既能看见子程序内部的寄存器变化又不会在系统代码里迷路。资源里演示的 2020 年调试过程就是靠着 T 和 P 交替使用才一步步把数据段、缓冲区、子程序入口都看了一遍。4.3 D 命令观察 BUF 里的 ASCII 与 W 里的十六进制年份输入年份之后程序把字符存进缓冲区。用 D 命令可以查看指定地址的内存内容-d 0C53:0050 0C53:0050 32 30 32 30 0D 00 00 ...左侧是段地址:偏移中间是字节的十六进制值右侧是能打印的 ASCII 字符。32H 对应字符230H 对应0所以32 30 32 30就是字符串 “2020”。转化完成之后W 里存的是 07E4H也就是十进制 2020。这个对比很直观ASCII 形式是给人看的字符十六进制值是给 CPU 算的数两者差一个30HDATACATE 做的正是这个减法。用 D 查看 W 时要注意地址。你可以先根据 .LST 查 W 的偏移或者直接看 DATACATE 最后MOV W,BX对应的地址。这个习惯能帮你少走很多弯路。4.4 E 命令改内存值绕过错误段单独验证后续逻辑调试到一半发现 DATACATE 因为循环问题结果不对但你想先确认 IFYEARS 是否正常这时候 E 命令就是后悔药直接修改内存把 W 改成期望的 07E4H。-e 0C53:0058 0C53:0058 00. E4 00. 07输入 E 和地址后Debug 会逐个字节显示当前值你输入新值再按空格继续回车确认结束。修改后继续用 G 或 T 跑就能绕过前面的转换错误只验证闰年判断和输出分支。资源演示里把 W 改成 07E5H2021 的十六进制继续运行就正常输出了 “This is not a leap year!”。需要提醒的是E 修改只对当前调试会话有效重新加载 EXE 后内存会恢复所以它适合用来“验证猜想”不适合用来“修 bug”。4.5 G、F、A、H、Q剩余上机指令的快速用法G 是直接运行命令可以不带参数全速运行也可以带地址运行到指定位置。资源里用 G 复验代码修改后的效果过程通常配合前面的单步定位一起用。F 用来把一段区域填充成相同数据适合清理缓冲区-f 0C53:0050 0057 32A 是内存汇编命令直接在内存里写指令。资源里用 A 写了一个 AX 存 10H、BX 存 5H、再求和的微型实验用于验证汇编逻辑-a 0C59:0100 0C59:0100 MOV AX,10 0C59:0103 MOV BX,5 0C59:0106 ADD AX,BX 0C59:0108H 用来计算两个十六进制数的和与差比如H 12 34会把 12H 与 34H 的和、差都显示出来。Q 是退出 Debug退出前注意把修改过的内存地址记下来复现时用得上。5. 避坑CX 高位残留导致循环失控五个 Debug 现场逐个拆5.1 坑位一循环结束后 BX 老是 71B0H所有年份结果都一样现象不管输入 2020 还是 2021用 T 单步走完 DATACATE 后BX 的值都是 71B0H结果每次都输出 “This is a leap year!”。原因主程序执行的是MOV CL,[BUF1]只把输入长度 04 送进了 CLCH 没有处理。Debug 加载 EXE 后CX 的初值是 0C59H 之类的高位残留于是 CX 变成了 0104H。LOOP 的判断条件是“CX 减 1 后是否等于 0”它先执行完正常的 4 次转换然后高位 01H 还在循环继续往下读缓冲区之外的内存。一共多跑了约 0x100 次256 次累加值超过 16 位后溢出低 16 位恰好固定为 71B0H。解决在取输入长度后补一条MOV CH,0。资源给出的修改就是这一句MOV CL, [BUF1] MOV CH, 0 ; 高位清零CX0004LOOP 只循环 4 次如果机器是 386 以上也可以用MOVZX CX, BYTE PTR [BUF1]一句到位但 8086/8088 没有 MOVZX实验课就老实写两条指令。5.2 坑位二直接运行正常、Debug 下必翻车寄存器初值不可依赖现象在 DOS 下直接运行 leapYear.exe输入 2020、2025、2100 结果全对但用 Debug 的 G 命令运行输出恒为闰年。原因直接运行时DOS 加载器留下的 CX 高位恰好是 0程序侥幸走对了Debug 加载 EXE 时CX 会被设成文件尾块大小调试过程中的 R 修改、G 跳转也会改变寄存器内容于是隐患立刻暴露出来。这不是 Debug 的锅是程序本身写了“依赖侥幸”的代码。解决所有循环计数寄存器在进入循环前显式初始化。不要赌“现在 CX 正好是 0004”要把“正好”变成“必然”。这也是资源里最核心的一条教训能运行出正确结果不代表程序没有 bug换个环境翻车只是时间问题。5.3 坑位三T 单步进入 INT 21H 后像进了黑匣子卡住出不来现象用 T 单步执行到 INT 21H屏幕输出或输入行为变得不规律接着进入大量看不懂的系统代码很难回到自己的程序。原因INT 21H 是 DOS 系统中断内部是成百上千条系统服务指令。T 会一条条执行这些指令你相当于钻进了操作系统的内部实现而不是用户程序。解决区分两个场景——想进入自己写的子程序CALL DATACATE用 T想跨过系统中断INT 21H用 P。这组搭配是 Debug 调汇编的标配。如果已经进去了可以用 G 加地址直接跳回用户程序的下一行别硬着头皮 T 到底。5.4 坑位四DIV 前 DX 没清零商溢出直接终止程序现象运行到 IFYEARS 的 DIV BX 时程序直接退出或者弹出除溢出异常。原因DIV 的除法指令用到 DX:AX 组成 32 位被除数。如果 DX 里残留了非零值被除数被放大商很容易超过 65535触发除溢出中断。这种问题在单步调试时最隐蔽因为 R 显示的 DX 看似“没用”实际上一直参与计算。解决除法前强制MOV DX,0。资源里 IFYEARS 的开头正确地写了这一句这是一个容易抄漏、抄漏后非常致命的地方。我每次看到 DIV 都会习惯性地找它前面的 DX 初始化语句。5.5 坑位五修改完源码却忘了重新编译链接还在调旧版本现象改了代码重新加载 EXE行为没有任何变化浪费半小时排查。原因只改了 .ASM 源文件没有重新跑 MASM 和 LINKDebug 加载的还是旧 EXE。或者跑了一次编译但 LINK 失败旧 EXE 被留在目录里。解决修改后固定走一遍三连masm leapYear; link leapYear; debug leapYear.exe进去先 U 看第一条指令或者用 D 查看数据段确认内容是自己刚改的版本。文件时间戳也可以佐证如果 EXE 时间比 ASM 还早多半是某一步没重跑。6. 修复后的回归验证把修改落成一套固定流程修完MOV CH,0之后不要只跑一次就收工。资源里最后重新编译、链接、运行验证了 2020、2021、2100 三个输入我把这套验证扩成一组更完整的回归测试覆盖闰年规则的三个边界普通闰年、普通平年、百年平年、四百年闰年。输入年份期望结果实际结果2020是闰年能被 4 整除、不能被 100 整除通过2024是闰年同上通过2021不是闰年不能被 4 整除通过2100不是闰年能被 100 整除但不能被 400 整除通过2000是闰年能被 400 整除通过验证命令同样固定C:\MASM5 masm leapYear; C:\MASM5 link leapYear; C:\MASM5 debug leapYear.exe进入 Debug 后先单步到MOV CL,[BUF1]和MOV CH,0执行完用 R 看 CX 是否为 0004。这个检查点很关键它直接证明循环次数从根源上被修正了。接着输入 2021走完整段程序确认输出是 “This is not a leap year!”再输入 2020确认输出是 “This is a leap year!”。到这里资源里那个“直接运行正常、Debug 下全变闰年”的诡异现象才算真正被解决。从那次以后我做汇编上机实验都会强制走一遍固定流程先用 R 看寄存器初值再进 DATACATE 确认循环计数然后用 E 改一次 W 单独验证 IFYEARS最后跑完整组闰年用例。这个流程帮我挡掉过好几次接口实验里的寄存器残留问题。希望帮到你。本文还有配套的精品资源点击获取