Control flow in assembly recap

You can now read every branch, loop, and switch a compiler emits. This chapter took the straight-line instruction reading from earlier chapters and added the machinery of decisions: flags, jumps, and the recognisable shapes they form. Let's review the key ideas from each lesson.

Compare and the Flags Register

cmp a, b computes a - b, sets the flags register, and throws the result away; test a, b does the same with a bitwise AND, and test reg, reg is the compiler's idiom for "is this value zero?". Four flags carry the answer: ZF (result was zero), SF (result was negative), OF (signed overflow), and CF (unsigned overflow, or borrow). A comparison only prepares the flags, so always read a cmp or test together with the jump that follows it, that pair tells you what question was really asked.

Unconditional and Conditional Jumps

jmp always transfers control; a conditional jump branches only when the flags match, and otherwise falls through. je/jz and jne/jnz read ZF, while js/jns read SF. The orderings come in two families, and the mnemonic reveals the signedness of the data: less/greater (jl, jle, jg, jge) means the compiler is treating values as signed, while below/above (jb, jbe, ja, jae) means unsigned. Compilers also routinely branch on the inverted condition, sometimes against an adjusted constant (score >= 50 becoming cmp ..., 49 / jle), so the jump you see is often the negation of the comparison you wrote.

How if/else Compiles

An if with no else is a cmp/test, a jump on the inverted condition that skips the body, and an after-label. An if/else adds an else-label and a trailing jmp at the end of the then block so both paths reconverge at an end-label:

    cmp  eax, 0
    jne  .Lelse     ; inverted condition skips the then block
    ...             ; then block
    jmp  .Lend
.Lelse:
    ...             ; else block
.Lend:

Block order in the listing need not match source order, so follow the labels, not physical position. At higher optimization levels a small if/else can become branchless: a conditional move like cmovge always executes and simply chooses whether to keep its result, leaving nothing for the CPU to mispredict.

How Loops Compile

A loop is a jump that points backwards: the back-edge, a conditional jump whose target label sits above it. The standard layout is bottom-test: init runs once, an initial jmp drops straight to the condition (so the loop can run zero times), the body sits at a label, and the condition at the bottom ends with the back-edge, costing just one branch per iteration. for and while compile to the same skeleton, a for merely adds the one-time init and the increment inside the body, and the exit test is an ordinary cmp plus conditional jump obeying all the signed/unsigned rules above.

Switch Statements and Jump Tables

A switch compiles to one of two strategies chosen by how dense the case values are. Dense cases become a jump table: a bounds check (often an unsigned ja, which catches both too-high and negative values in a single compare), a table lookup, and an indirect jump like jmp rax that lands directly on the matching case in constant time. Sparse cases become a chain of compares, a run of cmp/je pairs identical to an if/else if ladder. A jump whose target is a register is the giveaway that a table is in play.

Key Terminology

  • Flags register: The register where cmp and test record single-bit facts about a result
  • ZF (Zero Flag): Set when a result is exactly zero, i.e. when cmp's operands are equal
  • SF (Sign Flag): Set when a result is negative
  • OF (Overflow Flag): Set on signed overflow
  • CF (Carry Flag): Set on unsigned overflow (a borrow)
  • cmp: Subtracts its operands, sets the flags, and discards the result
  • test: ANDs its operands, sets the flags, and discards the result
  • Unconditional jump (jmp): Always transfers control to its label
  • Conditional jump: Branches only when the flags match; otherwise falls through
  • Signed jump family: jl, jle, jg, jge, ordering jumps for signed values
  • Unsigned jump family: jb, jbe, ja, jae, ordering jumps for unsigned values
  • Inverted condition: The compiler's habit of branching on the opposite of the source test to skip a block
  • Conditional move (cmov): A branchless instruction that keeps or discards a copy based on the flags
  • Back-edge: A conditional jump to a label above it; the defining feature of a loop
  • Bottom-test layout: Loop shape with the condition at the bottom and an initial jmp down to it
  • Jump table: A table of code addresses indexed by the switch value, reached via an indirect jump
  • Indirect jump: A jump whose target is held in a register (jmp rax) rather than a fixed label
  • Compare chain: A sequence of cmp/je pairs implementing a sparse switch

Looking Forward

You can now follow execution wherever it goes within a function. The next chapter follows it between functions: how the call stack grows and shrinks, what the prologue and epilogue boilerplate you have been skimming actually does, and the System V calling convention that dictates which registers carry arguments in and results out. The mysterious push rbp and edi you have been stepping past are about to make complete sense.