☰
DEBUG调试实验:汇编与接口课程中寄存器、内存与端口调试实战指南
2026/10/14 17:56:31 网站建设 项目流程

简介:面向北京交通大学“汇编与接口”课程的Debug调试实验文档,以leapYear.exe闰年判断程序为实例,从零演示汇编语言程序的编译、链接与运行方法,并深入剖析判断闰年的核心逻辑。文档分四个模块展开:首先介绍如何通过汇编器和链接器生成可执行文件,并给出命令行运行方式;其次对代码进行整体与逐句分析,帮助理解流程控制;接着在Debug工具中进行上机实践,分别测试闰年与非闰年输入,详细记录寄存器变化,同时介绍D、T、C、P等常用调试指令;最后针对代码中的逻辑错误和输入处理问题,演示修改、重新编译并验证的完整排错思路。资源包内含1个docx文档,文件大小78.94MB,目录结构清晰,步骤记录完整,既可用于课堂实验复盘,也适合自学汇编调试的初学者。目前已有521人学习,对于想要掌握底层程序调试和微机接口知识的学生很有参考价值。

1. Debug调试实验:汇编与接口课程里最容易被低估的一课

Debug调试实验是汇编与接口课程里第一个把“程序执行过程”完全摊开的实验。在集成开发环境里,你点一下运行键,屏幕给出结果,中间发生了什么全是黑匣子;进了 DEBUG 之后,寄存器、内存字节、端口状态全部摆在眼前,每敲一条命令,CPU 就真的执行一步,连 IP 指向哪条指令都能亲眼看到。这个实验看起来只是“调试”,实际解决的是三个具体问题:验证你写的算法流程对不对、确认内存里的数据有没有按预期变化、定位接口实验里“写了端口却没反应”到底是哪一层出了问题。新手在 DEBUG 里最常得到的教训是:程序“看起来没问题”和“寄存器状态真没问题”完全是两回事。适合正在学汇编与接口、准备上机考试或者被接口实验逼到抓狂的人,花一个晚上把 DEBUG 命令摸熟,后面写接口驱动会省出好几倍时间。

2. 先读懂 DEBUG 的命令体系:从 R/D/U 三个命令开始

DEBUG 的命令不多,但每个命令都对应一种“看现场”的手段。很多人上来就急着用 A 命令写代码,连寄存器怎么读都没搞清,结果程序跑飞了只能干瞪眼。我一般不建议跳步,先花十分钟把状态查看类命令用熟,再谈写代码和设断点。这一节先解决一件事:怎么让 DEBUG 告诉你它现在到底处在什么状态。

2.1 用 R 命令让寄存器当前值“开口说话”

R 命令是 DEBUG 里使用频率最高的命令,没有之一。直接敲 R 回车,DEBUG 会把全部寄存器当前值列成一行,包括 AX、BX、CX、DX、SP、BP、SI、DI、DS、ES、SS、CS、IP 以及标志寄存器。这一行信息在实验报告里非常有用,它记录了指令执行瞬间的“快照”。

我常和学生强调一个判断标准:调试接口实验时,如果 R 命令输出里某个寄存器的值和你的预期不一致,那问题大概率不在外设,而在你写的指令序列本身。寄存器是 CPU 和外设之间唯一的“对话窗口”,窗口里的值不可信,后面所有操作都不可信。

R 命令也支持单独修改某个寄存器,比如“R AX”回车后会提示你输入新值,输入十六进制数再回车就写进去了。这个功能在循环实验里特别常用,因为 DEBUG 不会在每次运行前自动把寄存器清零。你上一次 G 命令遗留的 CX 值,很可能就是你这次运行结果对不上的原因。

提示:修改 CS、SS、SP 这类寄存器时要格外小心,改错地址会让下一步 G 或 T 命令直接跑飞。日常调试我一般只改 AX、BX、CX、DX、SI、DI 和 IP。

2.2 用 D 命令检查内存,用 U 命令反汇编:数据与指令分开看

D 命令用来查看内存内容,格式是“D 段地址:偏移地址”。比如敲“D DS:0200”会显示从 DS:0200 开始的 128 个字节,左侧是绝对地址,中间是十六进制数据,右侧是这些字节对应的 ASCII 字符。很多新手第一次看到右侧乱码一样的内容会很困惑,其实那只是 DEBUG 把字节按 ASCII 解释给你看,实际数据是十六进制那一列。

U 命令用来反汇编,格式是“U CS:0100”,它会从指定地址开始,把内存里的二进制指令翻译成汇编助记符。这个命令的价值在于:你写进内存的代码到底对不对,A 命令输入时可能看走眼,但 U 命令会把每条指令的字节数和助记符原样显示出来。调试接口程序时,我几乎每次写完代码都会先 U 一遍,确认控制字写入指令的机器码长度正确。

D 和 U 的“默认段”不同,这也是一个容易混的细节。D 命令默认使用 DS 段,U 命令默认使用 CS 段。如果数据在数据段而代码在代码段,查看时一定要写清段前缀,否则你 D 出来的内存可能根本不是你的程序访问的那块。

2.3 DEBUG 命令一览:后面章节会反复用到的清单

命令作用典型用法注意事项
R查看/修改寄存器R 或 R AX修改 CS/IP 前先想清楚
D显示内存内容D DS:0200默认按 DS 段解释
U反汇编内存指令U CS:0100默认按 CS 段解释
A从键盘输入汇编指令A 0100直接写入内存,不产生文件
E修改内存字节E DS:0200 30 40一次可写多个字节
T单步执行一条指令T会进入 INT/CALL 内部
P单步执行,但跳过过程调用P不会进入 INT/CALL 内部
G连续运行到断点G CS:0110断点地址必须是指令首字节
N指定文件名N MYSUM.COM与 W 命令配合使用
W把内存写入文件W从 CS:0100 开始写
Q退出 DEBUGQ退出前确认是否需要保存

这张表不是让背下来的,而是用的过程中对照着查。后面各章的每个操作都会落到这张表里的命令上。

3. 在 DEBUG 里跑通一个循环累加程序:从 A 命令写代码到 G 命令设断点

这一章用一个最简单的循环累加程序,演示 DEBUG 里“写代码、核对代码、设断点、看结果”的完整链路。程序的功能是:把数据区里三个字节累加,把结果存到指定内存单元。选这个例子的原因是它涉及的寄存器少、循环跳转清晰,能同时体现 A、U、R、T、G、D 六个命令的配合。

3.1 用 A 命令从 CS:0100 开始写代码:内存即源文件

在 DEBUG 里写代码不用编辑器,A 命令会从你指定的地址开始,把你输入的助记符逐条翻译成机器码,写进内存。下面这段代码假设从 CS:0100 开始输入。

A 0100 MOV AL,00 MOV CX,0003 MOV SI,0200 ADD AL,[SI] INC SI LOOP 0107 MOV [0204],AL INT 20

三条前置指令的含义分别是:AL 清 0 作为累加器,CX 设为 3 作为循环次数,SI 指向 0200 作为数据区起点。ADD AL,[SI] 把一个字节数据加到 AL,INC SI 让 SI 指向下一个字节。LOOP 0107 里的 0107 是这条 LOOP 指令自身所在地址,CPU 执行 LOOP 时先让 CX 减 1,若 CX 非零则跳回 0107 继续循环。MOV [0204],AL 把最终累加结果写到 0204 单元。INT 20 是程序结束并返回 DEBUG 的标准指令。

输入时要注意:每行提示符会显示当前偏移地址,比如输完 MOV AL,00 后提示符变成 0103,表示下一条指令从 0103 开始。LOOP 指令的目标地址要填它自己所在的偏移,而不是循环里第一条指令的地址。很多人在这一步填错,填成 0103,实际就跳到了 MOV CX 那条指令,循环结构直接被破坏。

3.2 用 U 命令核对代码,用 R 命令设置初始寄存器

代码写完后先不要急着运行,用 U 命令把这段内存反汇编一遍,确认每条指令的机器码和助记符正确。

U CS:0100

如果发现某条指令和目标不同,可以用 A 命令在该地址重新输入覆盖,DEBUG 允许你从任意地址重新写指令,后面的字节除非被覆盖否则会保留下来。这个特性是双刃剑:方便修改,但也容易让内存里残留旧指令,U 命令核对这一步不能省。

运行前还有一步关键操作:把 R 命令调出来,确认 IP 指向 0100,并且 AX、CX、SI 这些寄存器没有被上一次运行污染。DEBUG 没有“重新加载程序”的概念,所有寄存器保持上次运行结束时的样子。

R IP 0100 R AX 0000 R CX 0000 R SI 0000

这里的 R IP 后面输入 0100,意思是把指令指针改到程序起点。如果漏了这一步,G 命令可能从任意位置开始执行,这是 DEBUG 调试里最隐蔽的“起始状态污染”问题。

3.3 断点组合拳:先 G 到循环结束,再 T 单步观察中间状态

程序整体运行用 G 命令,指定断点地址为 INT 20 所在位置。先用 U 命令确认最后一条 INT 20 的偏移地址,假设是 0110,然后执行:

G CS:0110

G 命令从当前 CS:IP 连续执行,遇到断点地址就停下来,显示所有寄存器的当前值。执行完这行命令后,用 D 命令查看 0204 单元,应该能看到 10 + 20 + 30 的结果,也就是 60。

D DS:0204 L4

这里 L4 是长度参数,表示只显示 4 个字节。如果结果正确,说明整个程序的“大流程”没问题;接下来想看循环到底是怎么一步步算出来的,就要把 IP 重置回 0100,再用 T 命令单步走几轮。

R IP 0100 T T T

每敲一次 T,DEBUG 执行一条指令并打印所有寄存器状态。重点看三处:AL 是否按预期累加、SI 是否每次加 1、CX 是否逐步递减到 0。第一次 T 后执行 MOV AL,00,AX 的高字节 BX 可能残留旧值,所以要看 AL 而不是 AX。这个区分对初学者是个很典型的认知门槛:在 DEBUG 里,寄存器是 16 位的,你操作的是 8 位寄存器时,另外 8 位不会自动清零。

4. 把 DEBUG 用到接口实验:用 IN/OUT 指令直接调试端口与外设

到了接口实验,DEBUG 的定位就不再是“验证算法”了,而是变成了一个能读写端口的调试终端。很多人刚进接口实验时,习惯把芯片初始化代码写一大段,然后运行完凭眼看结果对不对,出了问题完全不知道是控制字写错、端口基址算错,还是外设根本没响应。DEBUG 的解决办法很直接:程序里只写一条 IN 或 OUT,跑完立刻用 R 和 D 检查寄存器与内存。

4.1 在 DEBUG 里执行 IN/OUT:先读后写是接口调试的第一原则

DEBUG 命令本身没有 IN 和 OUT,但可以用 A 命令写一小段指令,再用 G 或 T 执行。假设实验箱上一个输出端口的地址是 0180H,想向它写入数据 06H,代码段如下:

A 0100 MOV DX,0180H MOV AL,06H OUT DX,AL INT 20 G

端口地址 0180H 超过了一个字节能表示的范围,所以不能写成“OUT 0180H,AL”,必须先把地址放到 DX,再执行 OUT DX,AL。这是一个语法细节,也是一道常见考题:只有端口地址在 00H 到 FFH 之间才能用立即数形式,超过就必须走 DX 间接寻址。

执行完 G 后,如果想把字节读回来验证写没写进去,可以在 INT 20 之前加一条 IN AL,DX,或者把这两步分开调试。我一般会先只写不读,用 R AX 确认 AL 的值,再用 IN 读回对比。读回值等于写入值,只能说明 CPU 总线和地址译码这一侧没毛病;外设有没有真正动作,还得看实验箱上的灯或数码管状态。

提示:端口操作没有“编译器帮你检查”的过程,写错地址也只是往某个不存在的 IO 地址发了个数据,DEBUG 不会报错。这种“静默失败”最容易让人误以为程序没问题。

4.2 8255/8253 这类接口芯片的调试顺序:控制字与端口基址是关键

接口实验里常见的 8255 并行接口芯片有四个寄存器要操作:三个并行端口(PA、PB、PC)和一个控制字寄存器。不同实验箱的端口基址分配不一样,同一个实验箱的 A0、A1 地址线接法也会影响端口偏移。我的调试顺序固定是下面三步。

第一步:查实验箱手册或原理图,确认控制字寄存器的绝对地址,先把初始化控制字写进去。比如要让 8255 工作在方式 0、PA 和 PB 均输出,控制字可能是 80H,写的时候必须先写控制口,再写数据口。控制字这一步的常见错误是顺序反了:先向 PA 口写了数据,此时 8255 还处于没有初始化的状态,数据根本不会出现在引脚上。

第二步:执行 OUT 指令,用 G 命令跑完,再用 IN 指令把该端口读回来。这是判断故障在 CPU 侧还是外设侧的分界线。回读结果正确,说明你对端口地址的估算、总线时序都没问题;回读结果不对,优先怀疑控制字本身每一位的定义,而不是怀疑芯片坏了。

第三步:如果回读正确但外设不动,去查两个东西:一是芯片的片选信号有没有被译码电路接通,二是输出引脚后级的驱动芯片使能端是否有效。这一层已经超出 DEBUG 能看到的能力了,DEBUG 只能帮你验证到端口寄存器这一级。

4.3 DEBUG 和高级语言调试器的本质差异:寄存器窗口就是实验现场

在集成开发环境里调试程序,你盯着变量名和调用栈;在 DEBUG 里,你能看到的东西只有寄存器、内存字节和端口状态。这个“退步”实际上是接口实验最需要的视角:8255 不认变量名,只认哪个引脚的电平怎么变化,而引脚的最终状态是由你写进端口寄存器的字节决定的。

因此我通常会把 DEBUG 的 R 命令输出当成实验报告的原始记录。不仅是调试过程本身,也是在报告里写“实验结果分析”时最直观的证据。把 G 命令执行后的寄存器快照贴出来,再配上当时端口的电平状态描述,老师一看就能知道你是在哪一个指令周期发现问题。比起整屏截图,R 命令单行文本也方便整理成文档。

5. DEBUG 调试实验避坑指南:五条高频翻车记录

这一章记录的是我在 DEBUG 调试过程中反复看到的高频问题。每一条都按“现象、原因、解决”的流程写,希望能帮你少走几趟弯路。

5.1 断点打在非法地址,G 命令直接“跑飞”

现象:执行 G 命令设断点后,程序没有停在你预期的位置,IP 寄存器跳到一个奇怪地址,屏幕输出花掉,甚至直接退出 DEBUG。

原因:DEBUG 的 G 命令实现断点的机制是在目标地址写入一个 INT 3 指令(机器码 CCH),执行到那里才停下。如果断点地址不是一条指令的首字节,而是指令中间的某个字节,CPU 就会把那个字节当成一条完整的指令解释,后续流程完全被打乱。

解决:设断点前先用 U 命令反汇编目标区域,确认断点落在一条指令的起始地址。更稳妥的做法是直接对齐到 INT 20 这种已知指令的地址,不要凭手工计算“大概在 0110 附近”。

5.2 用 T 命令单步执行 INT 21H,IP 跳进中断向量再也回不来

现象:T 命令单步到 INT 21H 这条指令时,寄存器输出突然变成一堆看不懂的地址,再按几次 T,系统像死机一样没有响应。

原因:T 命令的语义是“步入”,遇到 INT、CALL 这类指令时,会真正进入被调用的程序内部。INT 21H 是 DOS 系统中断,内部有大量指令,不可能一条条单步走完,而且 DEBUG 本身也被 DOS 托管,进了系统中断内部容易一起卡死。

解决:遇到已经确认没问题的中断调用,用 P 命令而不是 T 命令。P 命令会执行完整个 INT 21H 调用,停在它的下一条指令上。这是 T 和 P 在 DEBUG 里最本质的区别,也是很多人忽略的一个细节。

5.3 D 命令看到的数据和自己写入的不一致

现象:用 E 命令往内存写了 1234H,再用 D 命令查看,看到的却是“34 12”,以为自己没写对。

原因:x86 处理器采用小端存储方式,16 位数据的低字节放在低地址,高字节放在高地址。D 命令按地址从低到高逐字节显示,自然先显示低字节 34H,再显示高字节 12H。这不是写错了,是存储顺序问题。

解决:显示 16 位数据时,把左右两个字节倒过来理解即可。如果是调试异步串口这类按字节收发的外设,高低字节顺序尤其容易踩坑,发送前先想清楚协议约定的是哪个字节在前。

5.4 G 命令运行结果和 T 命令单步结果不一致

现象:用 T 命令一步步走,算法结果完全正确;改用 G 命令一次跑完,结果却错了,反复执行几次结果还不稳定。

原因:G 命令运行前没有复位 IP,导致程序不是从 0100 开始的;或者上一次运行留下的 AX、CX、SI 等寄存器值污染了本次初始状态。DEBUG 不会像现代调试器那样在每次运行前帮你重置现场。

解决:每次运行 G 之前,养成固定习惯:R IP 把指令指针指到程序起点,再用 R 命令手动把程序里用到的寄存器清零。这个过程确实烦,但它是 DEBUG 模式下保证结果可复现的唯一办法。

5.5 OUT 指令写了端口没反应,先读回再怀疑控制字

现象:写端口的指令执行成功,寄存器值也正常,但外设完全没反应,LED 不亮、数码管不跳。

原因:多半不是 DEBUG 的问题,而是写入的控制字不正确、端口地址没算准,或者外设侧需要额外的使能信号。DEBUG 只负责把数据放到总线上,不会替芯片解释控制字,更不会替外部电路接通电源。

解决:先加一条 IN AL,DX 把端口读回来,确认数据总线侧读写一致。如果读回正确,再逐位核对控制字含义;如果读回不正确,查地址译码和芯片片选。这两步能排除掉大多数“端口写不进”的假故障。

6. 把调试结果固化成 .COM:一处保存让每次验证不再重敲命令

DEBUG 里调试好的程序只存在于内存,Q 命令一退出,全部消失。下一次想验证,又得重新敲一遍 A 命令。我一般会在调试通过后,用 N 和 W 命令把内存中的程序保存成 .COM 文件,之后每次验证只需要一条命令。

先用 U 命令查看程序总长度,确认最后一条指令的结束偏移地址。假设程序从 0100 开始,最后一条 INT 20 的结束偏移是 0110,那么文件长度为 0010H 字节。保存步骤:

R BX 0000 R CX 0010 N MYSUM.COM W Q

R BX 和 R CX 指定文件长度,W 命令从 CS:0100 开始把内存内容写入当前目录下名为 MYSUM.COM 的文件。文件名的扩展名要写全,DEBUG 不会自动补。保存后退出 DEBUG,在 DOS 提示符下直接输入 MYSUM.COM 就能运行。

这个保存动作还有个额外价值:你可以拿它验证之前的调试结论是不是真的可靠。运行一遍 MYSUM.COM,再重新进入 DEBUG,用 D 命令查看数据区,如果结果一致,说明程序不依赖 DEBUG 里的手工寄存器复位,是“站得住”的。我现在做接口实验,还是会把 DEBUG 开在旁边,先用它把控制字、端口值调明白,再回到代码里做封装。这个习惯帮我少拆了很多次实验箱,也希望这篇笔记能帮你少踩几个坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询