7.1 汇编源程序、工具链与调试
第 6 章逐条研究了指令:操作数进入处理器,结果发生变化,标志位记录结果的性质。要组成真正可用的程序,还需要再加一层:给指令和存储位置命名,把名称变成地址,组合分别编写的部分,并在执行错误时观察机器状态。
本章使用 8086 的 Intel 风格汇编语法。MASM、TASM、NASM 与教学汇编器的伪指令拼写并不完全相同,但它们承担的基本任务一致。遇到工具特有写法时,我们关注它解决的问题,不假定所有汇编器都接受相同源码。
源文件同时包含指令与交给工具的说明
汇编源文件(assembly source file)是普通文本。MOV AX,BX 这样的指令要求处理器执行操作;.CODE、DB、END 这样的伪指令(directive)要求汇编器组织区域或生成字节,处理器不会执行它们。
符号(symbol)是与某个值关联的名称。标号(label)是附着在某个位置上的符号。在下面的小模块中,start、again 与 done 表示指令地址,count 表示一个数据字节:
.DATA
count DB 3
.CODE
start:
MOV CL,count
XOR AX,AX
again:
INC AX
DEC CL
JNZ again
done:
HLT
END startDB 表示定义字节(define byte):保留一个字节并设置初值。.DATA 与 .CODE 标识逻辑上的数据区和代码区。END start 告诉这种汇编器,程序入口符号是 start。这些伪指令都不会成为 8086 指令。
汇编器在概念上通常至少完成两遍处理:先发现符号及其偏移量,再使用这些值编码指令。因此,向前跳转可以在 done: 出现前引用 done;建立符号表后,汇编器就能填入位移。
汇编、链接与装入回答不同问题
对象文件(object file)保存已经编码的字节以及尚未最终确定的元数据,例如导出符号、外部引用和仍需修补地址的位置。链接器(linker)组合对象文件,选择最终布局,解析跨模块名称,并生成可执行映像。装入器(loader)把映像放入存储器并建立初始机器状态。
无效助记符或缺失的本地标号可以在单个模块汇编时诊断。外部符号在当前模块中可能合法,却始终没有提供者;这属于链接错误。能够成功构建但计算出错误结果的程序存在运行时逻辑错误,需要通过观察定位。
调试器把隐藏的执行变成证据
调试器(debugger)控制已经装入的程序,并暴露处理器状态。初学阶段最重要的操作有:
- 断点(breakpoint)在选定地址的指令执行前暂停。
- 单步执行(single-step)执行一条指令,然后再次暂停。
- 寄存器视图显示 IP、AX、SP、FLAGS 等寄存器值。
- 存储器视图显示选定地址的字节或字。
- 监视项(watch)在执行变化时持续显示指定寄存器或存储位置。
假设一个循环需要计算 :
MOV AX,3
XOR BX,BX
again:
ADD BX,AX
DEC AX
JNZ again
MOV [0200H],BXDEC AX 上的断点会在前一条 ADD 完成后、DEC 尚未执行时命中。第一次命中时,AX 仍为 3,BX 已经是 3。把“当前突出显示的指令”误当成“刚刚完成的指令”,会导致许多错误轨迹。
调试过程应当围绕假设展开:
1. 写出预期的不变量,例如“每次 ADD 之前,AX 是下一个正加数,BX 是当前累计和”。
2. 在可以检查不变量的位置设置断点。
3. 比较预期与实际的寄存器、标志、存储器和 IP。
4. 只单步到足以发现第一次分歧的位置。
5. 修正源码、重新构建,并再次执行同一测试。
第一次分歧比最终错误结果更重要。如果误写的 JZ 在第一次循环后退出,后面的存储器写入只是错误变得可见的位置;错误跳转才是程序首次偏离预定路径的位置。
现在你已经能区分源程序组织、汇编错误、链接错误与运行时证据。第 7.2 节将主动使用标号和条件跳转,构造完整的选择、循环与事件驱动状态机。