7.2 Branches, Loops, and State Machines
Section 7.1 showed how labels become addresses and how a debugger exposes the path actually taken. We can now design that path intentionally. A program is not merely a list of instructions: it is a graph of basic blocks, where each basic block has one entry, runs straight through, and ends by falling through or transferring control.
Build a decision from paths that rejoin
An if/else decision needs three parts:
1. A comparison that establishes flags.
2. A conditional transfer to one path while the other path is reached by fall-through.
3. A join label where both paths agree on the state expected by later code.
Suppose AL and BL are signed bytes and CL must receive their maximum:
CMP AL,BL
JGE use_al
MOV CL,BL
JMP done
use_al:
MOV CL,AL
done:The branch condition describes the path it enters: JGE use_al means “if AL is signed greater than or equal to BL, enter the block that selects AL.” When it is false, execution falls through to the block that selects BL.
Do not insert a flag-changing instruction between CMP and the conditional jump unless that new result is deliberately what the jump should test. MOV preserves arithmetic flags, but INC, SUB, AND, and many other instructions replace some or all of the evidence established by CMP.
The data interpretation is part of the program contract. Use JA/JAE/JB/JBE for unsigned ordering and JG/JGE/JL/JLE for signed ordering. The bit patterns F0H and 10H represent unsigned 240 and 16, but signed −16 and +16, so a correct unsigned maximum and signed maximum select different inputs.
A loop needs an invariant and a stopping argument
A loop invariant is a statement that should be true at a chosen point on every iteration. It gives a stable fact against which a trace can be checked. For a sum loop, an invariant might be: “at again, BX contains the sum of already processed values, and SI identifies the next value.”
A pre-test loop checks whether work remains before entering the body. It naturally handles an empty input:
JCXZ finished
again:
; process one item
LOOP again
finished:A post-test loop executes the body before checking. It is appropriate only when at least one iteration is guaranteed. Chapter 6 showed the dangerous boundary: entering a body with CX=0000H and then using LOOP wraps CX to FFFFH, causing 65,536 body executions.
Every loop design should answer three questions:
- Initialization: what state is true before the first iteration?
- Progress: which value moves toward termination on every path through the body?
- Termination: which comparison or count proves that no work remains?
Nested loops need independent counters or explicit save/restore. If an inner loop reuses CX, it destroys the outer LOOP counter unless the program preserves the outer value. The same ownership rule applies to pointer registers and accumulators.
A state machine makes long-lived control explicit
Some programs do not finish after one decision or one counted loop. A controller repeatedly waits for an event, combines it with its current state, performs an action, and selects a next state. A finite-state machine has a finite set of named states and a transition rule for every relevant state/event pair.
A transition table is often clearer and safer than a long chain of jumps. For a traffic controller, rows can represent RED, GREEN, and YELLOW while columns represent TIMER, EMERGENCY, and RESET. Each cell contains the next state and any output action.
The boundary case is an event that must work from every state. If EMERGENCY must always enter RED, write and test three explicit transitions. Repairing only the currently visible state leaves a latent defect that appears when the event arrives from another state.
You can now construct paths whose meaning remains visible at their join points and across repeated events. Section 7.3 applies these control structures to consecutive data: arrays, lookup tables, and strings.