Control Flow
Labels and Unconditional Jumps
Labels
A label marks a position in the code and gives it a name, which is how you refer to an address without computing it. Labels that begin with a dot are local — they belong to the previous non-local label, so you can reuse .loop in many functions without collision.
start: ; global label
xor rax, rax
.loop: ; local label, scoped to `start`
inc rax
cmp rax, 5
jl .loop ; jumps to start.loop
ret ; `start` and `.loop` are both reachable here
other: ; a new global label
.loop: ; a DIFFERENT .loop — no conflict with the one above
ret
jmp — Changing the Instruction Pointer
jmp label ; jump to a named position
jmp rax ; jump to the address held in a register (indirect)
jmp [table + rcx*8] ; jump to the address stored in memory (jump table)
jmp is how you leave the straight line of instructions. The indirect forms — through a register or through memory — are what make function pointers, jump tables, and virtual dispatch possible.
Conditional Jumps
A conditional jump does exactly one thing: it inspects the flags register and decides whether to move rip. It does not compare anything itself — that is cmp's job.
| Condition (in C) | Signed jump | Unsigned jump | Meaning of the mnemonic |
|---|---|---|---|
== | je | je | jump if equal (ZF = 1) |
!= | jne | jne | jump if not equal |
< | jl | jb | less (signed) / below (unsigned) |
<= | jle | jbe | less-or-equal / below-or-equal |
> | jg | ja | greater (signed) / above (unsigned) |
>= | jge | jae | greater-or-equal / above-or-equal |
== 0 | jz | jz | jump if zero — same as je |
| negative | js | — | jump if sign flag set |
| overflow | jo | jc | jump if overflow / carry |
jg when the values are unsigned, or ja when they are signed. Both compile, both run, and one of them is silently wrong for negative or large values. Decide the signedness of your data before you write the jump.
Building if / else
High-level languages present if as a block that runs when a condition is true. Machine code inverts that: you jump past the block when the condition is false. Learning to think in negations is the single biggest mental adjustment in assembly.
; C: if (rax > 10) { a(); } else { b(); }
cmp rax, 10
jle .else ; note the NEGATED condition: if NOT greater, skip a()
; --- then-block: rax > 10 ---
call a
jmp .end ; must jump over the else-block
.else:
; --- else-block ---
call b
.end:
Two habits avoid most control-flow bugs:
- Negate the condition you jump on. Falling through should mean "the condition was true", because that is the common case.
- Never forget the jump over the else-block. Omitting it runs both branches, and it will look correct in a small test.
Building Loops
A loop is the same conditional jump, aimed backwards. The reliable pattern — used by compilers and recommended here — is to test at the top and jump out, rather than to test at the bottom.
; C: for (i = 0; i < 10; i++) { work(i); }
xor rcx, rcx ; i = 0
.loop:
cmp rcx, 10 ; i < 10 ?
jge .done ; no -> exit the loop
; --- loop body ---
; work(i) goes here, using rcx as the counter
inc rcx ; i++
jmp .loop ; back to the test
.done:
Testing at the top means a loop with zero iterations simply skips the body, which matches the semantics of every mainstream language. If you test at the bottom (do { } while), the body always runs at least once — correct for that construct, wrong for a while.
The loop Instruction (and Why to Avoid It)
x86 has a dedicated loop instruction that decrements rcx and jumps if it is not yet zero:
mov rcx, 10
.repeat:
; ... body ...
loop .repeat ; rcx -= 1; if rcx != 0 jump back
It looks elegant but is a trap for beginners: loop is slower than dec + jnz on every modern CPU, it requires the counter to be in rcx specifically, and rcx is caller-saved — so it cannot survive a function call. Prefer the compare-and-jump pattern above.
Multi-Way Selection: Jump Tables
A chain of compares is fine for two or three cases. For many cases — a parser dispatching on an opcode, a virtual machine's interpreter loop — the idiomatic solution is a jump table: an array of code addresses you index directly.
section .rodata
table dq .case0, .case1, .case2, .case3 ; 8 bytes per entry
TABLE_N equ ($ - table) / 8 ; number of cases
section .text
; rax holds the selector (0..3)
cmp rax, TABLE_N
jae .default ; out of range -> default branch
lea rbx, [rel table] ; rbx = address of the table
jmp [rbx + rax*8] ; jump to the address stored at table[rax]
.case0:
; ... handle case 0 ...
jmp .end
.case1:
; ... handle case 1 ...
jmp .end
.case2:
; ...
jmp .end
.case3:
; ...
jmp .end
.default:
; ... handle anything else ...
.end:
Two details make this safe and fast. The bounds check (jae .default) is mandatory — without it, a bogus selector jumps into the middle of unrelated code. And the table holds addresses, so the elements must be dq (8 bytes) on x86-64.
Summary
- A label is a target;
jmpmoves execution to it unconditionally. - Conditional jumps read the flags;
cmp/testset them. - Signed and unsigned comparisons use different mnemonics —
jg/jlvsja/jb— even though both read the same flags. - An if/else is a negated conditional jump past the then-block; a loop is a conditional jump backwards.
- Multi-way selection is a jump table of code addresses, always preceded by a bounds check.
Next: Subprograms & the Stack — how call and ret turn jumps into functions.