How Branching Is Implemented in Assembly Language

How is branching implemented in Assembly language

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 JMP or B)
  • 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.

FlagNameSet when…
ZFZero FlagThe result of an operation is zero
SFSign FlagThe result is negative (high bit set)
CFCarry FlagAn unsigned operation overflowed/underflowed
OFOverflow FlagA signed operation overflowed
PFParity FlagThe 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:

MnemonicConditionTypical use
JE / JZZF = 1Equal / result is zero
JNE / JNZZF = 0Not equal / result is non-zero
JG / JNLEZF=0 and SF=OFGreater (signed)
JGESF = OFGreater or equal (signed)
JL / JNGESF ≠ OFLess (signed)
JLEZF=1 or SF≠OFLess or equal (signed)
JA / JNBECF=0 and ZF=0Above (unsigned)
JB / JNAECF = 1Below (unsigned)
JCCF = 1Carry set
JOOF = 1Overflow 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

Conditionx86 mnemonicARM mnemonicMeaning
EqualJE/JZBEQZero flag set
Not equalJNE/JNZBNEZero flag clear
Greater (signed)JGBGTGreater than
Less (signed)JLBLTLess than
Greater/equal (signed)JGEBGEGreater than or equal
Unsigned aboveJABHIHigher (unsigned)
Unsigned belowJBBLO/BCCLower (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

ApproachAdvantagesDisadvantages
Chain of conditional jumpsSimple, easy to readO(n) worst case, more branch mispredictions
Jump tableO(1) dispatch, very fast for dense casesRequires contiguous/dense case values, more memory for the table
Conditional move (branchless)No misprediction penaltyOnly suitable for simple, small conditional assignments
ARM predicated instructionsAvoids branch entirely for short sequencesLimited 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 stepi around a CMP/Jcc pair 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 JGE vs JG, or JLE vs JL.

Common Mistakes

  1. Confusing signed and unsigned conditional jumps (JG vs JA, JL vs JB) — a frequent source of bugs when comparing values that could be negative.
  2. Forgetting that CMP sets flags based on subtraction ordercmp eax, ebx followed by jg branches if eax > ebx, not the reverse; getting this backwards inverts your logic.
  3. Off-by-one errors in loop bounds, often from using JG/JL when JGE/JLE was intended, or vice versa.
  4. Assuming flags persist across unrelated instructions — many instructions (like MOV) do not affect flags, but others you might not expect (like AND, OR, INC in 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
Total
0
Shares

Leave a Reply

Previous Post
What is the purpose of the assembler directive in Assembly language

What Is the Purpose of the Assembler Directive in Assembly Language?

Next Post
Define the term opcode in Assembly language

What Is an Opcode? Defining and Understanding Opcodes in Assembly Language

Related Posts