Changing Where Execution Goes

Normally the CPU runs one instruction after another, top to bottom. A jump breaks that flow: it sets the instruction pointer to a label somewhere else, so the next instruction executed is the one at that label rather than the one physically below. Every if, else, for, while, and switch you have ever written eventually becomes some arrangement of jumps. This lesson catalogues them.

There are two kinds: the unconditional jump, which always goes, and the conditional jumps, which go only when the flags say so.

jmp: the Unconditional Jump

jmp .L3 means "continue from label .L3, no questions asked". It always transfers control. You saw it in the previous lesson, on the line that skips over the else block:

    mov  eax, 1
    jmp  .L3
.L2:
    mov  eax, 0
.L3:

Once eax has been set to 1, the jmp .L3 leaps over the mov eax, 0 so that the "return 0" path is not also executed. An unconditional jump is how the compiler stitches together blocks that must not fall into one another.

Conditional Jumps Read the Flags

A conditional jump takes the branch only if the flags are in a particular state, the state left behind by a preceding cmp or test. If the condition is not met, the CPU simply falls through to the next instruction. The mnemonic spells out the condition:

Jump Takes branch when... Reads
je / jz equal / zero (ZF set) ZF
jne / jnz not equal / not zero (ZF clear) ZF
js result was negative SF
jns result was not negative SF

je and jz are literally the same instruction with two names: "jump if equal" and "jump if zero" describe the same flag (ZF), because cmp a, b sets ZF when a - b is zero, i.e. when a == b. Use whichever name reads more naturally for the surrounding code.

Two Families: Signed and Unsigned

The comparisons that get interesting are the orderings, less-than and greater-than. Here x86-64 has a fork in the road: there are two complete sets of conditional jumps, one for values the compiler is treating as signed and one for unsigned. They exist because "less than" means different things for the two interpretations. The bit pattern 0xFFFFFFFF is -1 as a signed int but 4294967295 as an unsigned one, so a single set of jumps could not possibly serve both.

The signed family uses the words less and greater:

Jump Branch when (signed)
jl / jnge less than (<)
jle / jng less than or equal (<=)
jg / jnle greater than (>)
jge / jnl greater than or equal (>=)

The unsigned family uses the words below and above:

Jump Branch when (unsigned)
jb / jnae below (<)
jbe / jna below or equal (<=)
ja / jnbe above (>)
jae / jnb above or equal (>=)

This is one of the most useful tells in all of assembly reading. If you see jl, jg, jle, jge, the compiler is treating the values as signed. If you see jb, ja, jbe, jae, it is treating them as unsigned. The same C++ comparison operator can compile to either, depending on the types involved, so the jump mnemonic quietly tells you the signedness of the data.

Under the hood, the signed jumps inspect SF together with OF, while the unsigned jumps inspect CF (and ZF for the "or equal" variants). You rarely need to trace the exact flag logic; recognising which family a jump belongs to is what matters.

Seeing the Signed Family in Action

Here is a variant of the classify function from the last lesson. This time the source compares score >= 50:

int classify(int score)
{
    if (score >= 50)
        return 1;
    else
        return 0;
}
classify(int):
    push rbp
    mov  rbp, rsp
    mov  DWORD PTR -4[rbp], edi
    cmp  DWORD PTR -4[rbp], 49
    jle  .L2
    mov  eax, 1
    jmp  .L3
.L2:
    mov  eax, 0
.L3:
    pop  rbp
    ret

Two things are worth noticing. First, the jump is jle, from the signed family, because score is an int. Second, the compiler rewrote the test: the source asked score >= 50, but the assembly compares against 49 and jumps with jle (less than or equal). It inverted the condition so the "false" path is the one that branches away. score >= 50 is the opposite of score <= 49 for integers, so jumping to the return 0 block on score <= 49 produces identical behaviour. Compilers do this rephrasing constantly; the jump you see is often the negation of the comparison you wrote.

A Quick Reference for Reading

When you meet a conditional jump, ask three questions:

  1. What flag(s) does it read? That tells you what cmp/test was really checking.
  2. Signed or unsigned? less/greater means signed; below/above means unsigned.
  3. Is it the inverted condition? Compilers often branch on the opposite of the source test to skip a block.

Answer those and any branch becomes readable, no matter how the C++ above it was written.

Key Takeaways

  • jmp is unconditional: it always transfers control, typically to skip over a block.
  • Conditional jumps branch only when the flags match; otherwise execution falls through.
  • je/jz and jne/jnz test ZF (equality / zero); js/jns test SF (sign).
  • There are two ordering families: signed (jl, jle, jg, jge) and unsigned (jb, jbe, ja, jae).
  • The mnemonic reveals signedness: less/greater = signed, below/above = unsigned.
  • Compilers frequently branch on the inverted condition, comparing against an adjusted constant to skip the false path.