简介:这份《【汇编与接口】Debug调试实验》配套文档面向学习汇编语言与微机接口的本科生及自学者,依托北京交通大学课程实验场景,围绕闰年判断程序展开调试训练,帮助读者理解汇编程序的编译链接、逐段逻辑分析、断点与单步执行、错误定位和修改后重新构建验证等完整流程。压缩包内仅含1个docx文档,约78.94MB,文档按实验记录组织,目录覆盖编译链接运行、代码逻辑分析、Debug程序演示及代码错误修改四部分,便于按实验步骤查阅。已有523人学习。文档在Debug程序部分分别展示是闰年与不是闰年的输入路径,并整理D、T、C、P等常用上机指令的用途;代码错误修改部分则给出问题定位、条件修正及重新链接编译运行的验证思路,适合课程实验复盘和汇编调试入门练习。
1. 调试闰年判断程序:输入 2021 却输出“是闰年”的玄学现场
一个判断闰年的汇编程序,直接双击运行,输入 2020 显示“是闰年”,输入 2021 显示“不是闰年”,一切正常。可一旦拖进 Debug 里用 T、P、G 单步跑,输入 2021 竟然也吐出一句“This is a leap year!”——同一份 exe,命令行下是对的,调试器里是错的。这种“加调试就变味”的故障,是汇编与接口类课程实验里最常见的翻车现场之一,也是很多人第一次真正意识到「寄存器初值」能决定程序生死的地方。如果你正卡在 Debug 单步跳到一半、LOOP死活不退出、BX里蹦出一个莫名其妙的71B0,这篇排错记录基本就是照着你的现场写的。下面从编译链接、代码逻辑、单步现象一路走到根因和修复,每一步都给可复现的操作和判断依据。
2. 现象复现:编译链接能过,Debug 里结果全错
先把现场固定下来,否则后面所有讨论都是空谈。这个程序的结构很典型:DATA段放提示字符串和输入缓冲区,STACK段 200 字节,CODE段里主程序调用两个子程序——DATACATE负责把用户输入的四位年份字符串转成十六进制数存进变量W,IFYEARS负责判断闰年并用CF进位标志位回传结果,主程序靠JC A1决定输出哪句话。
2.1 编译、链接到生成 leapYear.exe 的完整路径
汇编工具链对路径极其敏感,很多人第一步就卡在masm找不到文件。我的习惯是把整个 masm5 目录放到 C 盘根目录,路径短、无空格、无中文,避免汇编器把路径当参数截断。进目录、汇编、链接三步走:
# 进入工具目录,路径保持短且无空格 cd C:\masm5 # 汇编:生成 leapYear.obj,同时按参考书要求建立交叉索引文件 masm leapYear.asm; # 链接:生成 leapYear.exe,并产出 .map 内存映射便于后续对照 link leapYear.obj;masm后面那个分号表示“其余提示项全部取默认值”,省掉一路回车;link同理要加;。链接成功后目录里会多出.obj、.lst、.ref、.map、.crf、.exe这一串文件,其中.lst列表文件能看到每条指令的偏移地址,.map能看到段基址——后面在 Debug 里对地址时全靠它们。
提示:如果
masm报 “unable to open file”,九成是当前目录不对或者文件名大小写与实际不符,先dir *.asm确认再执行。
2.2 直接运行与调试运行的差异从哪来
直接运行时输入 2020、2026、2025、2100,输出全部符合闰年规则,说明业务逻辑(能被 4 整除、不能被 100 整除、或能被 400 整除)本身没写错。差异只出现在 Debug 环境下,这就把怀疑范围压缩到了「运行环境带来的寄存器/内存初值差异」上。
关键点在于:DOS 加载 exe 时会按 exe 头里的信息设置CS:IP、SS:SP,但不会把所有通用寄存器清零。直接运行时CX的初值恰好是 0,而进入 Debug 后R命令一看,CX根本不为 0。程序里DATACATE子程序结尾用的正是LOOP X3,而LOOP的循环条件是「CX减 1 后不为 0」——它看的是完整的 16 位CX,不是低 8 位CL。
; 主程序中的关键两行:只写了 CL,CH 保持原样 MOV CL, [BUF+1] ; 把输入字符个数送到 CL,高位 CH 未处理 LEA DI, BUF+2 CALL DATACATE ; 子程序内部用 LOOP 控制循环次数BUF的布局是DB 8 / DB ? / DB 8 DUP(?),BUF+1存的是实际输入长度,输入 2020 就是 4。这条MOV CL, [BUF+1]只改了CX的低 8 位,高 8 位CH保留着进入程序时的随机值。只要CH ≠ 0,那么CL从 4 减到 0 之后CX仍然不是 0,LOOP会继续跑,直到CX整个减到 0 才停。这就是“直接运行碰巧对、Debug 里必错”的全部来源——一个没被清零的高位寄存器,典型的寄存器初值踩坑。
2.3 用 R、U、T、P、G 把错误现场钉死
复现路径要能被人照着走一遍,命令顺序很关键:
# 进入调试器加载目标程序 debug leapYear.exe # R:看寄存器,重点记录 CX 初值、CS:IP -r # U:反汇编,拿到每条指令的偏移地址,和 .lst 对照 -u # G:直接跑到输入处(不要在 INT 21H 上死磕 T) -g 0012 # 输入 2021 后继续单步,观察 BX 与 CX 的变化 -tT单步遇到INT 21H会陷进中断服务程序里出不来,这是新手最容易崩心态的地方;要跨过中断和子程序调用,改用P。P会把整个INT 21H或整个CALL当作一条指令执行完再停,T则会钻进去。D命令用来看内存,比如-d 0C53:0050能看到刚才输入的年份以 ASCII 形式躺在缓冲区里(32 30 32 30就是 “2020”)。E命令能直接改内存,-e 0050 32 30 32 31就地把输入改成 2021,省得反复重启程序。
3. 根因拆解:CX 高位未清零,循环次数失控
现象锁定后,需要把“为什么会算错”讲到底,否则改完这行下次还会在别处犯。
3.1 LOOP 指令到底看哪几位
LOOP的语义是CX = CX - 1,若CX ≠ 0则跳转。注意判断的是整个 16 位寄存器。输入 2020 时CL = 4,若CH = 0,循环恰好 4 次;若CH = 0x71(Debug 下实测),CX从0x7104开始减,要减0x7104次才归零。每轮循环都在做BX = BX * 10 + 当前位,跑到第 5 位以后读到的已经是缓冲区之外的数据,乘 10 累加一路溢出,最终BX停在71B0。
0x71B0的十进制是 29104,它非 0 且能被 4 整除,于是IFYEARS里的第一次除法DIV BX(BX = 4)余数为 0,接着除以 100 余数也不为 0,走到LAB2执行STC把CF置 1,主程序JC A1跳去打印“是闰年”。所以不管输入什么年份,只要CX高位脏,结果恒为闰年——这不是闰年算法错了,是送进算法的数错了。
; DATACATE 子程序的核心循环,问题就在循环次数来源 X3: MOV AL, [SI] SUB AL, 30H ; ASCII 转数值 MOV AH, 0 XCHG AX, BX MOV DX, 10D MUL DX ; BX * 10 XCHG AX, BX ADD BX, AX ; 加上当前位 INC SI LOOP X3 ; CX 减 1 不为 0 就继续 —— 看的是完整 16 位 MOV W, BX RETMUL DX是 16 位无符号乘法,结果放在DX:AX,这里用XCHG来回倒腾是为了把累加值一直留在BX。整段逻辑本身没问题,问题在进入循环前没人保证CH = 0。
3.2 为什么直接运行不会翻车
exe 被 DOS 加载时寄存器内容取决于加载器和调用方,直接双击运行时那份环境恰好让CX高位为 0,纯属运气。Debug 加载后会先执行自己的初始化流程,CX被写成了别的值(实测高字节非 0)。这解释了“命令行对、调试错”的反直觉现象,也解释了为什么很多同学在机房机器上跑得好好的,拷回自己电脑一进 Debug 就全错——依赖未初始化寄存器的程序,结果由环境决定,不由代码决定。
同样的道理适用于AX、DX、SI、DI:凡是子程序里要读写却没先赋值的寄存器,都属于这一类隐患。IFYEARS开头老老实实PUSH BX/CX/DX再MOV AX, W、MOV CX, AX、MOV DX, 0,所以它稳;DATACATE只对SI、BX、AX、DX赋了值,唯独循环计数器例外。
3.3 修复:一行 MOV CH,0 与它的替代写法
最小改动是在MOV CL, [BUF+1]之后补一条清高位指令:
MOV CL, [BUF+1] ; 取输入长度 MOV CH, 0 ; 关键修复:把 CX 高 8 位清零,保证 LOOP 次数正确 LEA DI, BUF+2 CALL DATACATE等价的写法有三种,按场合选:
| 写法 | 指令 | 说明 |
|---|---|---|
| 单独清高位 | MOV CH, 0 | 改动最小,语义最直白,推荐 |
| 先清整个再赋值 | MOV CX, 0后MOV CL, [BUF+1] | 多一条指令,但杜绝遗漏 |
| 用字节操作扩展 | MOVZX CX, BYTE PTR [BUF+1] | 32 位汇编可用,16 位实模式下不支持 |
改完必须重新汇编链接,否则跑的仍是旧 exe:
masm leapYear.asm; link leapYear.obj; debug leapYear.exe重新进 Debug 后先R看CX,再G跑到输入处、输入 2021,此时CX应为0004,LOOP四轮后BX = 07E5,IFYEARS里07E5 / 4余 1,JNZ LAB1跳走执行CLC,主程序不跳A1,输出“This is not a leap year!”。这一步验证通过,说明根因确实只有一个。
4. 避坑清单:Debug 实验里最容易踩的五个坑
这些坑我在不同机器、不同版本的 masm 环境里反复见过,按「现象 → 原因 → 解决」列清楚,遇到了直接对号入座。
坑一:T单步卡在INT 21H出不来。现象是按了十几次T,屏幕上一直是中断服务程序的指令,再也回不到自己的代码。原因是T会陷入中断向量指向的中断处理程序内部逐条执行。解决方法是遇到INT 21H和CALL一律改用P,P把整条调用当作一步走完;如果已经陷进去了,用G加断点地址跳出来,别硬按T。
坑二:G命令直接跑飞,输出乱码或直接返回 DOS。现象是-g回车后程序瞬间结束,看不到任何提示。原因是没设断点,G从当前CS:IP一直执行到程序终止。解决办法是用G 偏移地址指定停止位置,比如-g 0012停在输入调用之前,再逐步往后推。
坑三:U反汇编的地址和.lst对不上。现象是列表文件里某条指令在0010,U显示却在0013。原因是段基址不同,U默认从当前CS:IP开始,而.lst里的偏移是相对段首的。解决方法是先R记下CS值,再用U CS:0000这种带段前缀的形式反汇编,或者直接比对偏移量而非绝对地址。
坑四:改了源码没重新链接,Debug 里行为一点没变。现象是按修复方案加了MOV CH,0,重新进 Debug 还是老结果。原因是只跑了masm没跑link,或者link时输出到了别的目录,Debug 加载的还是旧 exe。解决办法是每次改完固定执行「汇编 → 链接 → 看时间戳」三步,用dir leapYear.exe确认文件时间是刚刚。
坑五:用E命令改内存验证后忘了改回来,误以为程序已修好。现象是手动把W改成07E4后一切正常,就下结论说代码没问题。原因是E只改内存,不改 exe,重启程序立刻打回原形。解决办法是把E只当验证手段:先用它确认“把输入换成正确值后程序是对的”,再把结论落回源码修改,两者不能互相替代。
5. 快速定位这类问题:先看初值,再看循环,最后看标志位
把这套流程固化下来,以后遇到任何“直接跑对、调试跑错”的汇编程序,都能在十分钟内定位。顺序是固定的:寄存器初值 → 循环控制变量 → 标志位传递。
第一步永远是进 Debug 后先按R,把AX、BX、CX、DX、SI、DI、SP、CS:IP全部抄在纸上或记在文本里,尤其是CX。这一步花三十秒,能省掉后面半小时的瞎猜。对照.lst里的指令偏移,确认自己停在了预期位置。
第二步,找到所有LOOP、JCXZ、DEC CX / JNZ这类依赖计数器的指令,逐个回溯「这个计数器在进入循环前最后一次被谁写过」。如果最后一次写的是 8 位操作(MOV CL, x、ADD CL, x),而进入循环用的是 16 位判断,那基本可以判定是高位残留。常见做法是在循环前加一句MOV CX, 0再赋低位,彻底断掉残留的可能。
第三步,看子程序之间靠什么传结果。这个程序用CF传闰年标志,那就要确认IFYEARS所有出口路径都显式设置过CF:LAB1有CLC,LAB2有STC,两条路都不会漏。如果某条分支既没CLC也没STC,CF就带着上一条指令的残留值回到主程序,JC的判断就成了掷骰子。
# 定位模板:进调试先记录,再定点停,最后验证标志位 debug leapYear.exe -r # 记录初值,重点 CX、CS:IP -u CS:0000 # 带段前缀反汇编,和 .lst 对齐 -g 0012 # 停在输入调用前 -p # 跨过 INT 21H,输入年份 -t # 单步进 DATACATE,盯 CX/BX一张对照表能把常见症状压成速查:
| 观察到的现象 | 优先怀疑 | 验证命令 |
|---|---|---|
| 循环次数明显多于输入位数 | CX高位未清零 | -r看CX初值 |
BX停在71B0之类的固定怪值 | 累加溢出且循环失控 | -t跟 3 轮看BX |
| 无论输入什么都判闰年 | CF被无条件置 1 | -t看STC是否必经 |
| 单步进不去子程序 | 用了P而非T | 换T重试 |
| 改了源码行为不变 | 未重新链接 | dir *.exe看时间戳 |
从那以后我每次写带循环的汇编子程序,都会在进入循环前强制走一遍「MOV CX, 0→ 赋循环次数」这个动作,哪怕明知道上一步刚清过;Debug 里第一件事也永远是R抄初值,绝不靠猜。寄存器不会骗人,它只会安静地保留上一个人留下的垃圾,等你在最关键的那次演示里翻车。希望帮到你。
本文还有配套的精品资源,点击获取