7.2 分支、循环与状态机
第 7.1 节说明了标号怎样变成地址,以及调试器怎样暴露实际执行路径。现在可以主动设计这条路径。程序不只是指令列表,而是由基本块(basic block)组成的图;每个基本块只有一个入口,内部顺序执行,最后通过落空或控制转移离开。
用重新汇合的路径构造选择
一个 if/else 选择需要三个部分:
1. 建立标志位的比较。
2. 通过条件转移进入一条路径,另一条路径由顺序落空到达。
3. 两条路径对后续代码所需状态达成一致的汇合标号。
假设 AL 与 BL 是有符号字节,CL 必须得到两者的最大值:
CMP AL,BL
JGE use_al
MOV CL,BL
JMP done
use_al:
MOV CL,AL
done:跳转条件描述它进入的路径:JGE use_al 表示“如果 AL 按有符号解释大于等于 BL,就进入选择 AL 的基本块”。条件不成立时,执行落入选择 BL 的基本块。
除非条件跳转就是要检查新结果,否则不要在 CMP 与跳转之间插入修改标志的指令。MOV 保持算术标志,但 INC、SUB、AND 等许多指令会替换 CMP 建立的部分或全部证据。
数据解释方式属于程序契约的一部分。无符号大小关系使用 JA、JAE、JB、JBE;有符号大小关系使用 JG、JGE、JL、JLE。F0H 与 10H 的位模式按无符号解释为 240 与 16,按有符号解释却是 −16 与 +16,因此正确的无符号最大值和有符号最大值会选择不同输入。
循环需要不变量与停止理由
循环不变量(loop invariant)是在每次迭代的某个固定位置都应当成立的陈述。它为轨迹检查提供稳定事实。对于求和循环,不变量可以是:“到达 again 时,BX 保存已经处理过的值之和,SI 指向下一个值。”
前测试循环(pre-test loop)在进入循环体前检查是否还有工作,因此可以自然处理空输入:
JCXZ finished
again:
; 处理一项
LOOP again
finished:后测试循环(post-test loop)先执行循环体,再检查条件。只有确定至少需要一次迭代时,它才合适。第 6 章已经展示危险边界:CX=0000H 时先进入循环体,随后使用 LOOP,CX 会回绕为 FFFFH,使循环体总共执行 65,536 次。
每个循环设计都应回答三个问题:
- 初始化:第一次迭代前,哪些状态为真?
- 推进:循环体的每条路径上,哪个值都在接近终止条件?
- 终止:哪个比较或计数证明工作已经完成?
嵌套循环需要独立计数器,或明确保存与恢复计数器。如果内层循环复用 CX,就会破坏外层 LOOP 的计数,除非程序先保存外层值。指针寄存器与累加器也遵循相同的所有权规则。
状态机把长期控制过程显式化
有些程序不会在一次选择或一次计数循环后结束。控制器不断等待事件(event),把事件与当前状态(state)组合,执行动作,再选择下一状态。有限状态机(finite-state machine)只有有限个命名状态,并为每个相关的状态/事件组合规定转移。
转移表通常比很长的跳转链更清楚、更安全。对交通控制器,可以让表的行表示红灯、绿灯和黄灯,列表示 TIMER、EMERGENCY 与 RESET。每个单元格保存下一状态和需要执行的输出动作。
边界情况是必须从每个状态都生效的事件。如果 EMERGENCY 必须总是进入红灯,就应明确写出并测试三条转移。只修复当前可见状态会留下潜伏缺陷;当事件从另一状态到达时,缺陷才会暴露。
现在你已经能构造在汇合点和重复事件之间仍保持清晰含义的路径。第 7.3 节将把这些控制结构应用于连续数据:数组、查找表与字符串。