恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
汇编Debug实战:从编译到单步排错闰年程序
首页
资讯中心
/
汇编Debug实战:从编译到单步排错闰年程序
汇编Debug实战:从编译到单步排错闰年程序
发布时间:2026/10/17 17:00:28
简介这是一份面向北京交通大学《汇编与接口》课程实验的Debug调试实训报告围绕leapYear.exe闰年判断程序完整记录从编译、链接、运行到逐句分析代码逻辑的过程。内容涵盖Debug常用调试指令如D显示内存、T单步执行、C继续至断点、P执行机器码指令、输入“是闰年/不是闰年”两种场景下的执行路径观察以及针对程序条件判断和输入处理问题的定位、修改与重新编译链接验证适合正在学习汇编语言与微机接口、需要上手Debug调试的本科生作为操作参考。包内目前包含1个docx格式实验文档压缩包大小约78.94MB文档目录清晰按“编译链接运行—代码逻辑分析—Debug程序—代码错误修改”四个模块展开既可作为上机实验的步骤复盘也能作为遇到同类调试问题时的排错思路索引。该资源已有521人学习下载对希望理解汇编程序底层执行机制、提升Debug调试能力的学习者具有实用参考价值。1. 用 Debug 调通 leapYear.exe从编译到“找茬”的一整条链路很多人学汇编与接口课程时最大的坎不是写不出代码而是程序跑出错误结果后不知道去哪看。这份“汇编与接口 Debug 调试实验”正好卡在这个痛点上它带着你从 masm 编译、链接生成 leapYear.exe 开始到用 Debug 单步跟踪闰年判断的完整执行流程最后还专门设计了一个“代码里藏着 bug”的环节逼着你用调试手段把问题揪出来。适合两类人一类是刚接触汇编、对段寄存器、INT 21H、标志位还发懵的初学者另一类是已经能写简单汇编程序但一遇到“结果不对”就只会瞎改代码、不会系统排查的同学。整个资源的核心价值不在于教你闰年怎么判断而在于帮你建立“用 Debug 看寄存器、看内存、看标志位”的调试习惯——这才是后续所有汇编实验能不能顺利推进的分水岭。2. 编译链接与代码逻辑先把“程序应该怎么走”钉死2.1 用 masm5 手动编译链接每一步生成什么文件都要心里有数实验的第一步不是写代码而是把源代码变成可执行文件。资源里给出的典型流程是先把 masm5 文件夹放到 C 盘根目录然后打开命令行进入该目录用 masm 命令对源码文件进行汇编汇编通过后再用 link 命令链接。cd C:\masm5 masm leapYear.asm link leapYear.obj汇编阶段输入源文件名 leapYear.asmmasm 会生成 leapYear.obj 目标文件如果指定生成交叉索引文件还会出现 .lst 列表文件和 .crf 交叉引用文件。链接阶段输入 leapYear.objlink 会生成 leapYear.exe 可执行文件。此时你会看到 masm5 文件夹里同时出现 .obj、.lst、.ref、.map、.crf、.exe 等文件这些文件各有用途.lst 是把源程序、地址和机器码对照排列的列表文件调试时想快速查某条指令的机器码就翻它.map 是内存映像文件记录了段的名称为起始地址及长度.crf 是交叉引用文件在大型程序里用来查某个符号在哪些地方被引用过。这里有个新手容易忽略的点masm 和 link 都是交互式命令执行后会提示输入文件名如果你直接回车它会用默认名或者报错。习惯做法是在命令行里把源文件名作为参数一次带全例如masm leapYear.asm;分号表示后面的提示都用默认值。链接时同样可以link leapYear.obj;省去逐项确认。生成 exe 后直接输入leapYear回车即可运行程序会先显示Please input a year:输入 2020 输出This is a leap year!输入 2026 和 2025 都输出This is not a leap year!输入 2100 也输出不是闰年。这个结果验证了程序的闰年判断逻辑能被 4 整除但不能被 100 整除或者能被 400 整除才是闰年。2100 能被 4 和 100 整除但不能被 400 整除所以不是闰年。2.2 数据段与代码段的核心设计DATACATE 和 IFYEARS 怎么分工代码的段结构是典型的汇编程序模板DATA 段放提示字符串和缓冲区STACK 段放 200 字节堆栈CODE 段放主程序和两个子程序。数据段里几个关键变量需要先记牢变量大小作用INFONDB存放提示输入年份的字符串YDB存放“是闰年”的输出字符串NDB存放“不是闰年”的输出字符串WDW存放转换后的十六进制年份BUFDB 8, ?, 8 DUP(?)输入缓冲区第一个字节是最大长度 8第二个字节是实际输入长度主程序的执行路径很清晰。先MOV AX, DATA再MOV DS, AX这里必须分两步因为 CPU 不允许把立即数直接送入段寄存器只能先送通用寄存器过渡。然后用LEA DX, INFON配合MOV AH, 9和INT 21H输出提示字符串这一组指令是 DOS 显示字符串的标准套路AH 9 表示显示字符串DX 指向以$结尾的字符串首地址。接着用LEA DX, BUF配合MOV AH, 10和INT 21H接收键盘输入AH 10 是带缓冲的键盘输入功能会把输入的字符放入 BUF 指向的缓冲区BUF1 位置存放实际输入字符数BUF2 开始存放字符本身。主程序里有三个关键细节值得展开讲。第一个是MOV CL, [BUF1]它把实际输入字符数送入 CL但这个操作只改了 CL 低 8 位没有把 CH 高位清零——这正是后面 Debug 调出来翻车的根因后面专门开一节讲。第二个是LEA DI, BUF2DI 指向输入字符串的首字符后面的 DATACATE 子程序就是从这里开始逐字符转换。第三个是主程序调用 IFYEARS 后用JC A1判断进位标志 CFCF 1 跳去输出“是闰年”CF 0 则顺序执行输出“不是闰年”这个设计把子程序的判断结果通过标志位传递出来符合汇编里“子程序用寄存器或标志位返回结果”的常规做法。2.3 DATACATE 字符串转数字乘十累加法的逐句拆解DATACATE 子程序的核心任务是把 BUF 里存的 ASCII 码字符串比如2020转换成真正的十六进制数值存入 W。这个转换算法叫“乘十累加法”流程是每读到一个字符先减 30H 把 ASCII 码变成数字再把之前累加的结果乘 10最后加上新读到的数字。整个过程循环四次正好处理四位年份。DATACATE PROC NEAR LEA SI, BUF2 ; SI 指向输入字符串首字符 MOV BX, 0 ; BX 作为累加器初始为 0 X3: MOV AL, [SI] ; 取一个字符的 ASCII 码 SUB AL, 30H ; ASCII 码减 30H 得到数字 MOV AH, 0 ; AX 高位清零AX 数字 XCHG AX, BX ; 暂存累加结果到 AX MOV DX, 10D ; DX 10 MUL DX ; AX AX * 10 XCHG AX, BX ; 乘完的 AX 与 BX 交换回来 ADD BX, AX ; BX 旧累加值*10 新数字 INC SI ; SI 指向下一个字符 LOOP X3 ; CX 减 1非零则继续循环 MOV W, BX ; 最终结果送入 W RET DATACATE ENDP这段代码在 Debug 里反复出现必须理解每一步对寄存器的改变。第一次循环SI 0050AL 2的 ASCII 码 32H减 30H 后 AL 02HAX 0002H。XCHG 后 AX 和 BX 交换此时 BX 0AX 2所以交换后 AX 0BX 2。MUL DX 后 AX 0 * 10 0再 XCHG 回去AX 2BX 0ADD 后 BX 0 2 2。第二次循环读到0BX 变成 (2 * 10) 0 20即十六进制 0014H。第三次循环后 BX 202十六进制 00CAH。第四次循环后 BX 2020十六进制 07E4H。到这里字符串2020就变成了数值 07E4H 存到 W 中。这里有个细节初学者容易卡住为什么用 XCHG 而不是直接 MUL BX因为 MUL 指令的乘数如果放在寄存器里那被乘数默认在 AX 中乘积的高 16 位存 DX、低 16 位存 AX会覆盖 AX 里原有的累加值。所以代码先把累加值从 BX 换到 AX乘完再把结果换回 BX这个交换操作就是一种保护现场的手段。2.4 IFYEARS 闰年判断嵌套分支与标志位的传递IFYEARS 子程序的逻辑是典型的嵌套条件判断。先把年份送入 CX 保存一份副本因为后面连续做三次除法会反复覆盖 AX 和 DX。然后判断能否被 4 整除不能就直接 CL C 清零退出能的话判断能否被 100 整除不能被 100 整除说明是闰年STC 置位退出能被 100 整除则还要看能否被 400 整除能的话置 CF 1不能的话 CF 保持为 0。IFYEARS PROC NEAR PUSH BX PUSH CX PUSH DX MOV AX, W ; 年份送入 AX MOV CX, AX ; CX 保存年份副本 MOV DX, 0 MOV BX, 4 DIV BX ; AX / 4AX商DX余数 CMP DX, 0 ; 比较余数和 0 JNZ LAB1 ; 余数不为 0不能被 4 整除CF 清零退出 MOV AX, CX MOV BX, 100 DIV BX CMP DX, 0 JNZ LAB2 ; 不能被 100 整除是闰年CF 置 1 退出 MOV AX, CX MOV BX, 400 DIV BX CMP DX, 0 JZ LAB2 ; 能被 400 整除是闰年 LAB1: CLC JMP LAB3 LAB2: STC LAB3: POP DX POP CX POP BX RET IFYEARS ENDP这里有个容易误读的地方DIV 指令对操作数长度的要求很严格。DIV BX 是字除法被除数默认是 DX:AX 组成的 32 位数所以执行前必须手动把 DX 清零否则 DX 里残留的旧值会影响商和余数的正确性。资源里在每次 DIV 前都执行MOV DX, 0这不是多余操作而是字除法前的必要初始化。另外LAB2 里没有 JMP 指令而是直接“落入”LAB3这也是汇编里常见的技巧——多个出口汇聚到统一的恢复现场代码避免在每处都重复写 POP 和 RET。这个子程序的两个关键设计点一是用 CX 保存年份副本因为第一次 DIV 后 AX 变成了商第二次、第三次判断需要原始年份没有副本就得重新从 W 里读二是子程序开头 PUSH 三个寄存器、结尾 POP 三个寄存器保持进入和退出时寄存器状态一致避免破坏主程序正在使用的数据。判断结果通过 CF 传递主程序一条JC A1就能完成分支整个接口设计简洁且不易出错。3. Debug 单步跟踪从 T 指令翻车到抓住 CX 高位没清零3.1 进入 Debug 与反汇编阅读R、U、T、P 四条命令的正确用法要调试 leapYear.exe先确认 MASM 环境正常然后输入debug leapYear.exe进入调试模式。资源里的实践顺序明确先用 R 命令查看寄存器初始状态CX 并不是 0IP 指向 0000CS:IP 构成了当前代码地址再用 U 命令反汇编把内存里的机器码翻译成汇编指令。debug leapYear.exeR 命令显示寄存器时会看到CX 0000之类的初始值但这个值是调试器加载程序时设置的不一定和真实运行环境一致。U 命令典型输出格式是地址 机器码 汇编指令比如0C59:0000 B8XXXX MOV AX,XXXX其中 B8 是 MOV AX, imm16 的机器码。Debug 里的所有地址都由段地址和偏移地址两部分组成CS 提供段基址IP 提供段内偏移CPU 用CS * 16 IP计算物理地址。T 命令是单步执行执行一条指令后停下来显示所有寄存器的当前值。P 命令也是单步执行但遇到 CALL 或 INT 指令时不会进入内部而是把它当作一步执行完毕。这个区别在调试时必须分清想进入子程序逐条观察内部逻辑就用 T想跳过子程序直接看调用结果就用 P。资源里记录了一个翻车经历调试时用 T 命令执行 INT 21H 中断结果程序进入 DOS 中断内部不再返回调试会话直接卡死改用 P 命令执行同样的 INT 21HDOS 中断被整体执行完程序停在下一指令输出正常。这说明 P 命令的“过程调用不进入”特性对 INT 指令同样生效。D 命令用来查看指定地址的内存内容格式是D 段地址:偏移地址显示结果左边是十六进制字节右边是对应的 ASCII 字符。资源里用D 0C53:0050查看输入年份的存储情况看到 2020 的 ASCII 码 32 30 32 30 存在连续四个字节里。对初学者来说D 命令最大的价值是把“内存里到底存了什么”从抽象概念变成亲眼可见的字节序列。3.2 DATACATE 的 Debug 现场一步步从 ASCII 变 07E4H调试 DATACATE 时T 命令单步进入子程序。关键观察点有三个第一个是进入子程序后IP 指向的并不是第一条执行指令而是跳过PROC伪指令直接到LEA SI, BUF2第二个是每执行一条指令AX、BX、DX 都在变化需要对照源码理解变化原因第三个是 LOOP 跳回 X3 标签时 CX 减 1从 0004 逐步减到 0000。资源里完整记录了 2020 的转换过程。T 单步执行到MOV CL, [BUF1]后CL 04此时 CX 0004CH 为 0一切正常。进入 DATACATE 后 SI 0050指向 BUF2。第一次执行MOV AL, [SI]AX 低 8 位变为 32H2的 ASCII执行SUB AL, 30H后 AL 02HAH 00HAX 0002H。XCHG 交换后BX 里暂存的就是这个数字 2。每次循环结束检查 BX第一次后 BX 2第二次后 BX 0014H即 20第三次后 BX 00CAH即 202第四次后 BX 07E4H即 2020。这个递增过程直观反映了乘十累加法的每一步。用 T 单步执行 LOOP 指令时会发现一个细节当 CX 减到 0 后LOOP 指令不会再跳回 X3而是顺序执行下一条指令MOV W, BX。但资源里记录了一次翻车在某次调试中T 命令执行完第四次循环后CX 显示 0000但 LOOP 仍然继续跳转回 X3导致循环没有正常退出。原因在于调试过程中 CX 的值被外部因素改了具体后面单独讲。如果用 P 命令调试由于 P 不进入子程序无法观察内部状态所以调试 DATACATE 这类核心转换逻辑T 命令更合适。3.3 闰年输入的完整跟踪从 DIV 到 CF 置位的标志位变化调试 2020 这个闰年输入时重点观察标志位的变化。进入 IFYEARS 子程序后AX 07E4H2020CX 保存同样的值BX 4DX 0。执行DIV BX后AX 07E4H / 4 01F9H505DX 0。CMP DX, 0比较后 ZF 1Debug 显示为 ZRJNZ 指令检查 ZF 0 才跳转此时 ZF 1 所以不跳转继续执行。第二次除法把 AX 恢复为 07E4HBX 100DX 0执行后 AX 07E4H / 100 0019H25DX 0014H20。CMP DX, 0后 ZF 0Debug 显示为 NZJNZ LAB2 跳转执行 STC 把 CF 置 1Debug 显示 NC 变 CY。继续执行落到 LAB3POP 恢复三个寄存器RET 返回主程序。主程序执行JC A1CF 1 所以跳转输出This is a leap year!。这个跟踪过程有两个关键结论。第一判断能否被 100 整除时得到的余数 DX就是年份去掉百位和千位后的后两位2020 的后两位是 20这个余数是否为零决定了是否能被 100 整除。第二三个子程序出口 LAB1、LAB2、LAB3 的 CF 状态分别是 0、1、保持原值而 LAB1 和 LAB2 最终都会落到 LAB3 统一恢复现场再返回。3.4 非闰年输入引出问题循环为什么跳不出去调试 2021 时问题浮出水面。前面四位数转换的过程和 2020 完全一样只是最后 BX 07E5H2021。但第四次循环结束后LOOP 仍然跳回 X3 继续执行BX 不断累加最终得到 71B0H换算成十进制是 29104这个值被存入了 W。而 29104 可以算出它能被 4 整除、也能被 100 整除却也能被 400 整除所以无论如何判断都显示闰年。这就是为什么在 Debug 中输入任何四位年份结果都显示闰年的原因。根因是 CX 高位的问题主程序MOV CL, [BUF1]只设置了 CL 4但没有设置 CH 0。如果进入 DATACATE 之前 CX 恰好等于 0004那循环四次后 CX 0 正常退出但如果调试过程中 CX 高位值不为 0比如 CX 0104H那么 LOOP 每执行一次 CX 减 1从 0104H 递减要到 256 次循环才减到 0这期间 BX 已经累加了远远超过四位年份的正确值最终超过 FFFFH 只保留低 16 位算出 71B0H。因为循环次数过多高位溢出后的结果与输入年份无关只与循环次数的低 16 位有关所以不同输入年份在 Debug 下都是同样的错误结果。验证方法在资源里给了两种一种是用 E 命令直接修改 W 的内存值为 07E5H然后继续运行程序输出This is not a leap year!说明 IFYEARS 本身逻辑没错另一种是用 R 命令把 CX 改成 0000 再继续执行程序也能得到正确结果。这两种验证都把问题定位到 CX 高位没清零而不是闰年判断逻辑出错。4. 避坑排查Debug 调试里的五个经典翻车现场4.1 现象INT 21H 单步执行后程序卡死用 T 命令单步执行到 INT 21H 指令时程序进入中断服务程序后不再返回调试界面卡住无法继续按预期逐条执行后续指令。原因INT 21H 是 DOS 系统调用中断T 命令会进入中断内部的代码而这些代码属于 DOS 内核不是用户程序的一部分调试器无法用单步方式逐条跟踪系统内核指令并且内核代码执行完后返回地址的衔接方式和普通 CALL 不同导致跟踪状态错乱。解决遇到 INT 21H、INT 20H 这类软中断指令时改用 P 命令。P 会把整个中断调用当作一条指令执行中断结束后自动停在 INT 21H 的下一条指令程序流程清晰可控。从此以后我每次调试遇到 INT 指令都强制走一遍“先看是不是中断是就用 P”不再碰 T。4.2 现象LOOP 循环不退出BX 值疯狂增长到 71B0H用 T 命令单步调试 DATACATE 时循环执行完四次后 LOOP 仍然跳回循环体BX 值从 07E4H 一路增大到 71B0H循环次数远超过预期。原因LOOP 指令以整个 CX 作为循环计数器执行一次减 1。主程序只给 CL 赋值CH 高位没有被清零。如果 CH 不为 0比如 CH 01、CL 04循环要执行 0x0104 次才能退出远超四位年份转换所需的 4 次。BX 每次循环累加后最终溢出只保留低 16 位形成 71B0H。解决在MOV CL, [BUF1]后加一条MOV CH, 0把 CX 高位强制清零。修改后循环恰好执行四次DATACATE 能正确转换年份。这个 bug 在直接运行程序时不会暴露因为非调试环境下进入 DATACATE 前 CX 恰好是 0004H但在 Debug 的 G 命令或其他场景下 CX 高位值可能是任意值所以必须在代码层面根除不能依赖调试环境的偶然正确性。4.3 现象G 命令运行程序结果错误但单独运行 exe 结果正确用 G 命令一次性运行整个程序并输入年份后程序输出“是闰年”的错误结果但退出 Debug 后直接运行 leapYear.exe输入同样的年份输出正确。原因Debug 加载程序时对寄存器做了初始化CX 的值可能不为 0。G 命令开始执行后主程序MOV CL, [BUF1]覆盖了 CL但 CH 保持 Debug 环境的值导致 DATACATE 的循环次数出错。直接运行 exe 时DOS 加载程序前的 CX 值恰好是合理的 0004H所以不触发问题。解决不要在调试环境里依赖“恰好合理”的寄存器初始值。对于这类需要固定循环次数的代码必须在进入循环前显式初始化完整的计数器寄存器只设置低 8 位或高 8 位都是隐患。用 G 命令之前先执行 R 命令检查 CX并把 CX 改为 0000能临时规避问题但正确做法是修改源码。4.4 现象DIV 指令计算结果完全错误商和余数都对不上在 IFYEARS 子程序里连续执行三次 DIV 指令第二次、第三次算出的余数明显不对比如 2020 除以 100 的余数不是 20 而是其他值。原因DIV BX 是 32 位除以 16 位的除法被除数默认是 DX:AX。第一次 DIV 后 AX 变成商、DX 变成余数但在第二次执行 DIV 前只重新加载了 AX 为年份值没有把 DX 清零DX 里残留上一次除法的余数导致被除数变成了高 16 位不为零的非法值商和余数自然全错。解决每次 DIV 之前明确执行MOV DX, 0并且从 W 重新读取年份到 AX。资源里代码已经写好了这个初始化但如果是自己手写同类代码很容易漏掉。调试时用 R 命令观察执行 DIV 前后的 DX:AX 值确认被除数是否符合预期这是排查除法问题的通用办法。4.5 现象输出字符串内容乱码或者显示不完整程序运行时提示信息正常但输入年份后输出结果里出现乱码字符或者字符串只显示一半就结束。原因INT 21H 的 AH 9 功能以$字符作为字符串结束标志。如果字符串定义里遗漏了结尾的$或者字符串中间不小心出现了$显示就会提前终止后续内容被系统当作乱码处理。另一个可能是 LEA DX 加载的地址不是字符串真正起始位置比如误把 BUF1 当成 BUF2。解决检查所有字符串定义末尾是否都有$Y 和 N 的定义要注意0DH, 0AH只是换行回车控制符不能代替结束符。用 D 命令查看字符串所在内存区域确认首地址到$之间的内容都是合法字符。如果地址偏移错了先数清楚 BUF 缓冲区的字节布局BUF0 是最大长度、BUF1 是实际长度、BUF2 才开始是输入数据。5. 进阶技巧与验证收尾把调试能力沉淀成固定动作5.1 用 E 命令做“代码级手术”修改内存绕过坏逻辑当代码里某个中间变量算错但你又不想整个程序重跑时用 E 命令直接修改内存里的数据是最快的验证手段。资源里反复用到这个技巧算出 W 的值不对就将 W 的内存强制改成 07E4H 或 07E5H再继续执行以此隔离问题范围——到底是转换环节坏了还是判断环节坏了。实际命令格式-E 0C53:0050 0C53:0050 07E4.07E5-E后面跟段地址和偏移地址回车后 Debug 显示当前字节内容输入新值再回车完成修改。这个做法的边界是修改的是内存中的临时数据不是源文件重启 Debug 后恢复原样。它能快速验证“假设某个变量正确后续逻辑是否正常”是做问题定位的高效手段。5.2 自己动手改源码加一条 MOV CH, 0 再走完整流程找到根因后修改源码是最直观的验证方式。原有代码里只有MOV CL, [BUF1]改成下面这样MOV CL, [BUF1] MOV CH, 0修改后重新执行编译和链接cd C:\masm5 masm leapYear.asm link leapYear.obj这里有个常见误区只重新编译不重新链接运行的是旧的 exe修改根本不生效。masm 生成新的 obj 后必须再 link 一遍覆盖旧 exe。改完重新运行输入 2020、2021、2100 三个年份分别得到“是闰年”“不是闰年”“不是闰年”结果全部正确。再用 Debug 的 G 命令跑一遍同样正确说明修复在调试环境下也生效。5.3 其他上机实践指令补全A、G、H、F 的边界在哪资源最后还整理了一组 Debug 的常用指令对后续做复杂实验很有用。A 命令从指定地址开始输入汇编指令Debug 会直接翻译成机器码存入内存适合小段代码临时验证。H 命令计算两个十六进制数的和与差比如-H 10 5输出0015 000B快速心算校验。F 命令把一片内存区域填充成同一个数据值如-F 0C53:0200 10 41做缓冲区初始化很方便。G 命令一次性运行到指定地址格式-G 地址比逐条 T 快得多。这些指令的组合使用才能真正提升调试效率先用 U 反汇编找关键代码地址再在合适位置用 G 跑到那里然后改用 T 单步观察细节发现内存异常用 D 查看、用 E 修改。这一套动作下来定位 bug 的时间能缩短一半以上。5.4 一个验证方法R 修改寄存器制造极端场景除了改内存修改寄存器的值同样能构造测试场景。比如用 R CX 把 CX 改成 0000验证循环是否会错误持续或者故意把 CX 改成 FFFF看程序如何崩溃反而能帮你理解寄存器和指令的交互机制。这类验证的价值在于你亲手制造了 bug才能对 bug 的成因有肌肉记忆。从那以后我每次写汇编循环都会强制走一遍“确认计数器高位清零、确认每次 DIV 前 DX 清零、确认字符串以$结尾”这三个检查再交付编译链接几乎没有再在调试上翻过车。希望这套 Debug 方法和排查思路对你有实际帮助。本文还有配套的精品资源点击获取