Branching is where Assembly programming stops feeling like a straight line and starts feeling like actual programming — loops, if/else logic, switch statements, all of it boils down to branching under the hood. When I first wrote an if statement’s worth of Assembly, I was honestly surprised how much was happening behind the scenes just to replicate something that’s one line in a high-level language. This post walks through exactly how branching works, from the flags register up through real conditional jump instructions on x86 and ARM.
What Is Branching?
Branching is the mechanism by which a program changes its execution path — instead of the CPU simply executing the next instruction in sequential memory order, a branch instruction can redirect execution to a completely different address, either unconditionally or based on some condition.
There are two broad categories:
- Unconditional branches — always redirect execution (like
JMPorB) - Conditional branches — redirect execution only if a specific condition is true (like
JZ,JNE,BEQ)
The Flags Register: The Foundation of Conditional Branching
Before a conditional branch can make a decision, the CPU needs something to base that decision on. On x86, this is the FLAGS register (EFLAGS/RFLAGS), a set of individual bits that get automatically updated by arithmetic and comparison instructions.
| Flag | Name | Set when… |
|---|---|---|
| ZF | Zero Flag | The result of an operation is zero |
| SF | Sign Flag | The result is negative (high bit set) |
| CF | Carry Flag | An unsigned operation overflowed/underflowed |
| OF | Overflow Flag | A signed operation overflowed |
| PF | Parity Flag | The low byte of the result has an even number of 1 bits |
cmp eax, ebx ; performs eax - ebx internally, discards the result, sets flags
je are_equal ; jump if ZF is set (i.e., eax == ebx)
The CMP instruction is really just a SUB that throws away the numeric result but keeps the flag updates — this is a subtle but important detail. TEST works similarly but performs a bitwise AND instead, commonly used to check if a value is zero or if specific bits are set.
test eax, eax ; sets ZF if eax == 0, without modifying eax
jz is_zero
Conditional Jump Instructions on x86
Here’s a practical table of the most commonly used conditional jumps:
| Mnemonic | Condition | Typical use |
|---|---|---|
JE / JZ | ZF = 1 | Equal / result is zero |
JNE / JNZ | ZF = 0 | Not equal / result is non-zero |
JG / JNLE | ZF=0 and SF=OF | Greater (signed) |
JGE | SF = OF | Greater or equal (signed) |
JL / JNGE | SF ≠ OF | Less (signed) |
JLE | ZF=1 or SF≠OF | Less or equal (signed) |
JA / JNBE | CF=0 and ZF=0 | Above (unsigned) |
JB / JNAE | CF = 1 | Below (unsigned) |
JC | CF = 1 | Carry set |
JO | OF = 1 | Overflow set |
Notice there are separate instructions for signed comparisons (JG, JL) versus unsigned comparisons (JA, JB) — this trips up beginners constantly. Using JL when you meant JB (or vice versa) produces correct results for some inputs and silently wrong results for others, especially near the boundary between positive and negative numbers.
; Example: if (a > b) { ... }
mov eax, [a]
cmp eax, [b]
jg a_is_greater ; signed comparison
; Example: if (unsigned_a > unsigned_b) { ... }
mov eax, [unsigned_a]
cmp eax, [unsigned_b]
ja a_is_greater ; unsigned comparison
Implementing an If/Else Statement
Let’s translate a simple high-level construct into Assembly, step by step.
if (x > 10) {
y = 1;
} else {
y = 0;
}
; x86-64 NASM
mov eax, [x]
cmp eax, 10
jle else_branch ; if NOT (x > 10), jump to else
mov dword [y], 1
jmp end_if
else_branch:
mov dword [y], 0
end_if:
Notice the pattern: the conditional jump tests the inverse of the condition you actually want, and jumps over the “then” block if that inverse is true. This inversion trick is universal across Assembly language and is one of the first things that feels unintuitive to newcomers coming from high-level languages.
Implementing a Loop
for (int i = 0; i < 10; i++) {
sum += i;
}
; x86-64 NASM
xor ecx, ecx ; i = 0
xor eax, eax ; sum = 0
loop_start:
cmp ecx, 10
jge loop_end ; if NOT (i < 10), exit loop
add eax, ecx
inc ecx
jmp loop_start
loop_end:
x86 also offers a dedicated LOOP instruction that implicitly uses ECX/RCX as a counter, decrementing it and jumping if it’s non-zero — a holdover from earlier, more accumulator/counter-centric design philosophy, though it’s rarely used in modern hand-optimized code because it tends to be slower than the equivalent DEC/JNZ pair on modern CPUs.
mov ecx, 10
repeat_loop:
; ... loop body ...
loop repeat_loop ; decrements ecx, jumps if ecx != 0
Branching on ARM
ARM handles conditional branching a bit differently — and more elegantly, in my opinion. Instead of relying purely on separate CMP-then-Jcc instruction pairs, most ARM instructions can be conditionally executed based on a 4-bit condition code embedded directly in the instruction itself (in AArch32; AArch64 restricts this mostly to branches and a few select instructions).
; ARM (AArch32)
CMP R0, #10
BGT greater_branch ; Branch if Greater Than (signed)
CMP R0, R1
BEQ equal_branch ; Branch if Equal
BNE not_equal_branch ; Branch if Not Equal
; AArch64
CMP X0, #10
B.GT greater_branch
B.EQ equal_branch
Condition Codes Comparison
| Condition | x86 mnemonic | ARM mnemonic | Meaning |
|---|---|---|---|
| Equal | JE/JZ | BEQ | Zero flag set |
| Not equal | JNE/JNZ | BNE | Zero flag clear |
| Greater (signed) | JG | BGT | Greater than |
| Less (signed) | JL | BLT | Less than |
| Greater/equal (signed) | JGE | BGE | Greater than or equal |
| Unsigned above | JA | BHI | Higher (unsigned) |
| Unsigned below | JB | BLO/BCC | Lower (unsigned) |
Internal Working: How a Conditional Branch Executes
flowchart TD
A[Compare/arithmetic instruction executes] --> B[Flags register updated: ZF, SF, CF, OF]
B --> C[Conditional jump instruction fetched]
C --> D{Does condition match flag state?}
D -->|Yes| E[PC/RIP updated to branch target]
D -->|No| F[PC/RIP advances to next sequential instruction]
E --> G[Fetch next instruction from new location]
F --> G[Fetch next instruction sequentially]
Branch Prediction: Performance Considerations
Modern CPUs use deep pipelines, meaning several instructions are being fetched, decoded, and partially executed at once. A conditional branch creates a problem: the CPU doesn’t know which path to fetch next until the condition is actually evaluated, which might be several pipeline stages later. To avoid stalling, CPUs use branch prediction — a hardware mechanism that guesses which way a branch will go, based on the branch’s history, and speculatively executes down that path.
- A correctly predicted branch costs essentially nothing extra.
- A mispredicted branch causes a pipeline flush — all the speculatively executed instructions are discarded, and execution restarts from the correct target, costing anywhere from a handful to 15–20+ cycles on modern CPUs.
This is why, in performance-critical code, minimizing unpredictable branches (or restructuring code to make branches more predictable — e.g., sorting data before a conditional-heavy loop) is a well-known optimization technique. Compilers and hand-optimizers sometimes use branchless programming (using bitwise tricks or conditional-move instructions like CMOVcc on x86 or CSEL on ARM64) to avoid branch misprediction penalties entirely in hot loops.
; Branchless max(a, b) using CMOVG instead of a conditional jump
mov eax, [a]
mov ebx, [b]
cmp eax, ebx
cmovg ecx, eax ; ecx = a if a > b
cmovle ecx, ebx ; ecx = b otherwise
Practical Use Cases
- Implementing control structures: if/else, switch/case (often via jump tables), for/while loops — all built entirely from conditional and unconditional branches
- Bounds checking: array access safety checks in hand-written low-level code
- State machines: parsers, protocol handlers, and interpreters implemented directly in Assembly rely heavily on branching to move between states
- Operating system context switching: kernel code uses conditional branches extensively when deciding which process to schedule next, checking privilege levels, and handling interrupts
Switch/Case via Jump Tables
For a switch statement with many cases, a chain of conditional jumps would be slow. Instead, Assembly (and compilers) often use a jump table — an array of addresses indexed directly by the switch value.
section .rodata
jump_table:
dq case_0, case_1, case_2, case_3
section .text
; assume eax holds the switch value, 0-3
jmp [jump_table + rax*8]
case_0:
; ...
jmp end_switch
case_1:
; ...
jmp end_switch
; etc.
end_switch:
This turns an O(n) chain of comparisons into an O(1) indexed jump — a great example of how understanding branching deeply lets you write genuinely faster code than a naive translation of high-level logic.
Comparing Branching Approaches
| Approach | Advantages | Disadvantages |
|---|---|---|
| Chain of conditional jumps | Simple, easy to read | O(n) worst case, more branch mispredictions |
| Jump table | O(1) dispatch, very fast for dense cases | Requires contiguous/dense case values, more memory for the table |
| Conditional move (branchless) | No misprediction penalty | Only suitable for simple, small conditional assignments |
| ARM predicated instructions | Avoids branch entirely for short sequences | Limited availability (mostly AArch32), can waste cycles executing “false” path |
Debugging Tips
- In GDB,
info registers eflags(x86) shows the current flag state, which is invaluable when a conditional jump seems to be going the “wrong” way. - Step through with
stepiaround aCMP/Jccpair to watch flags update in real time. - If a loop seems to run one too many or one too few times, check the exact condition — an off-by-one is almost always a mismatch between
JGEvsJG, orJLEvsJL.
Common Mistakes
- Confusing signed and unsigned conditional jumps (
JGvsJA,JLvsJB) — a frequent source of bugs when comparing values that could be negative. - Forgetting that
CMPsets flags based on subtraction order —cmp eax, ebxfollowed byjgbranches ifeax > ebx, not the reverse; getting this backwards inverts your logic. - Off-by-one errors in loop bounds, often from using
JG/JLwhenJGE/JLEwas intended, or vice versa. - Assuming flags persist across unrelated instructions — many instructions (like
MOV) do not affect flags, but others you might not expect (likeAND,OR,INCin some cases) do.
Best Practices
- Always double check signed vs. unsigned semantics before choosing a conditional jump mnemonic.
- For heavily nested conditionals, keep your labels descriptively named (
else_branch,loop_end) rather than generic (L1,L2) to keep the logic readable. - In performance-sensitive code, profile before reaching for branchless tricks — modern branch predictors are very good, and premature branchless rewrites can sometimes hurt readability without measurable gain.
- Use jump tables for dense switch/case-style dispatch instead of long conditional chains.
FAQs
Q: What’s the difference between JMP and Jcc (conditional jump)? JMP is unconditional — it always redirects execution. Jcc (like JE, JG, JNZ) only redirects execution if a specific flag condition is met; otherwise, execution falls through to the next instruction.
Q: Why does x86 have both JG and JA for “greater than”? Because “greater than” means something different for signed versus unsigned numbers, especially near the boundary where the highest bit flips (e.g., 0xFFFFFFFF is -1 signed, but the largest possible value unsigned). JG handles signed comparisons; JA handles unsigned.
Q: Does every instruction update the flags register? No — many instructions (like most MOV variants) leave flags untouched. Only specific instructions (arithmetic, logic, CMP, TEST, and others) update flags, and the exact set of flags each instruction affects is documented precisely in the architecture manuals.
Q: What is a mispredicted branch and why does it matter? It’s when the CPU’s branch predictor guesses the wrong direction for a conditional branch, forcing it to discard speculatively executed instructions and restart from the correct path — a real, measurable performance cost in tight loops with unpredictable conditions.
Summary and Key Takeaways
Branching is what turns straight-line Assembly code into real control flow — if/else statements, loops, and switch/case constructs are all built from conditional and unconditional jump instructions layered on top of a flags register (or, on ARM, condition codes baked directly into instructions). Getting comfortable with the flags register, the signed/unsigned distinction in conditional jumps, and techniques like jump tables and branchless programming will take you from someone who can follow Assembly control flow to someone who can write it efficiently and debug it confidently.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1, Chapter 3 (EFLAGS) and Volume 2 (Jcc instructions) — intel.com/sdm
- AMD64 Architecture Programmer’s Manual, Volume 1 — amd.com
- ARM Architecture Reference Manual, Condition Codes chapter — developer.arm.com
- Agner Fog’s optimization manuals (branch prediction and microarchitecture) — agner.org/optimize
- GNU Assembler (GAS) Documentation — sourceware.org/binutils
