Chapter 10 Key Takeaways: Control Flow

  1. All C control flow compiles to flag tests plus conditional or unconditional jumps. There are no if statements at the machine level. Every if, while, for, switch, and ternary operator becomes some combination of CMP/TEST and J-instruction.

  2. The conditional jump condition is the negation of the C condition. if (a > b) translates to cmp a,b; jle .else — the jump skips the if-body when the condition is false. Getting this inversion wrong is a common assembly bug.

  3. JL/JLE/JG/JGE are for signed comparisons; JB/JBE/JA/JAE are for unsigned comparisons. This is the most common control flow bug in assembly. Mixing them gives silently wrong results on values where signed and unsigned interpretations differ (e.g., 0xFFFF...FFFF is -1 signed and UINT64_MAX unsigned).

  4. JE and JZ are identical instructions. Both check ZF = 1. Similarly, JNE and JNZ are the same, and JC and JB are the same. The different names exist for readability: use JE after CMP (testing equality), JZ after arithmetic (testing zero), JC after unsigned arithmetic (testing carry).

  5. TEST rax, rax is preferred over CMP rax, 0 for zero-checks. They set the same flags, but TEST has a shorter encoding (no immediate byte). Similarly, xor eax, eax is shorter than mov rax, 0 and sets ZF.

  6. The do-while loop maps naturally to the bottom-test assembly structure (body first, then test, conditional jump back). The while loop and for loop require an initial condition check that may skip the body entirely.

  7. Counting down to zero saves one instruction per iteration compared to counting up, because the branch condition jnz is implicitly set by dec rcx without a separate CMP. This is valid when the iteration order can be reversed.

  8. Jump tables dispatch switch statements in O(1) time regardless of the number of cases. The pattern: bounds check (mandatory!), load address from table, indirect jump. Without the bounds check, an out-of-range value reads an arbitrary address from memory and jumps to it — a security vulnerability.

  9. Always use unsigned comparison (JA) for jump table bounds checks, even if the switch variable is a signed integer. This correctly rejects negative values, which would appear as very large unsigned numbers and index out of bounds.

  10. CMOV is a conditional move that does not branch. CMOVL rax, rbx moves RBX into RAX if SF ≠ OF (signed less than). It eliminates the branch misprediction penalty but always executes both the "move" and "no-move" paths, so it is only faster than a branch when the branch is unpredictable.

  11. CMOV is faster than a branch when the branch is ~50% unpredictable; a branch is faster when the prediction rate is >90%. Branch prediction is free when it works; the 10-20 cycle misprediction penalty is when CMOV wins.

  12. The LOOP instruction (decrement RCX, jump if not zero) is compact but slow on modern processors due to microcode overhead. Prefer dec rcx; jnz which achieves the same result in two fast instructions.

  13. Short jumps (8-bit offset, ±127 bytes) are 2 bytes; near jumps (32-bit offset) are 5 bytes. The assembler selects automatically. In very tight code (bootloaders, shellcode), keeping jump targets within ±127 bytes saves 3 bytes per jump.

  14. Indirect jumps (jmp rax, jmp [table + rdi*8]) are the mechanism behind virtual function dispatch, jump tables, and the ret instruction. Understanding them is essential for reading compiler output and for Chapter 35's return-oriented programming discussion.

  15. GCC generates jump tables when switch cases are dense (>~60% fill ratio) and numerous (>4-6 cases). Sparse switches get comparison trees. You can verify with Godbolt: add cases until the output switches from comparisons to a jump table.