Control flow#
Control flow in assembly is flags + jumps + calls.
Arithmetic and compare instructions set status bits in the
flags register; conditional jumps read those bits and branch.
call pushes a return address and jumps; ret pops the
address and returns. There is nothing else; if, while,
and switch are all open-coded out of these primitives.
For
cmp / test operand semantics, see Operators.
For call and stack frames in depth, see Functions.
Flags (x86-64 RFLAGS)#
Bit |
Meaning |
|---|---|
|
Zero Flag. Set when the result of the last arithmetic / logic instruction was zero. |
|
Sign Flag. Set when the result’s high bit was 1 (negative for signed). |
|
Carry Flag. Set on unsigned overflow / borrow. |
|
Overflow Flag. Set on signed overflow. |
|
Parity Flag. Set when the low byte of the result has an even number of 1 bits. |
|
Direction Flag. |
cmp a, b sets flags as if a - b had been computed (no
destination written). test a, b sets flags as if a AND b
had been computed.
Unconditional jump#
jmp .label ; near jump
jmp rax ; indirect through a register
jmp qword [rdi + 8] ; indirect through memory
Conditional jumps (x86-64)#
Read the flags set by the last arithmetic / compare. Two families: signed and unsigned.
Mnemonic |
Meaning |
|---|---|
|
jump if equal / zero ( |
|
jump if not equal ( |
|
jump if sign / not sign ( |
|
jump if carry / no carry ( |
|
jump if overflow / no overflow |
|
unsigned above / above-or-equal |
|
unsigned below / below-or-equal |
|
signed greater / greater-or-equal |
|
signed less / less-or-equal |
Memorise: a / b (above / below) = unsigned;
g / l (greater / less) = signed. Picking the wrong
family is the most common bug in hand-written x86-64.
; if-else
cmp rdi, 10
jl .less ; signed
jmp .greater_eq
.less:
; rdi < 10
.greater_eq:
; rdi >= 10
The skeleton: if / else#
flowchart TD
A([cmp rdi, 0]) --> B{jg ?}
B -->|signed > 0| C[".positive: call positive"]
B -->|else| D{je ?}
D -->|== 0| E[".zero: call zero"]
D -->|else| F[fall through: call negative]
C --> Z([.done])
E --> Z
F --> Z
; if (x > 0) positive(); else if (x == 0) zero(); else negative();
cmp rdi, 0
jg .positive
je .zero
; fall through: negative
call negative
jmp .done
.positive:
call positive
jmp .done
.zero:
call zero
.done:
The skeleton: while#
flowchart TD
A([xor rcx, rcx]) --> B[.loop]
B --> C[cmp rcx, rsi]
C --> D{jge .done?}
D -->|i >= n| E([.done])
D -->|else| F[body]
F --> G[inc rcx]
G --> H[jmp .loop]
H --> B
; while (i < n) { body; i++; }
xor rcx, rcx ; i = 0
.loop:
cmp rcx, rsi ; i vs n
jge .done ; signed; jae for unsigned
; body
inc rcx
jmp .loop
.done:
The skeleton: for#
C-style for (init; cond; step) body is the same as while,
with the increment at the bottom.
xor rcx, rcx ; init: i = 0
.loop:
cmp rcx, rsi ; cond: i < n
jae .done
; body
inc rcx ; step: i++
jmp .loop
.done:
The loop instruction#
x86-64 has a loop instruction that decrements rcx and
jumps if non-zero. Avoid it; modern CPUs are no
faster on loop than on the explicit dec / jnz
sequence, and the explicit form reads better.
mov rcx, 10
.loop:
; body
dec rcx ; rather than `loop .loop`
jnz .loop
switch#
Two forms. Cmp chain for sparse keys; jump table for dense keys.
; sparse switch
cmp rdi, 1
je .case_1
cmp rdi, 2
je .case_2
jmp .default
; dense (jump table)
cmp rdi, MAX
ja .default
jmp qword [rel .table + rdi*8]
section .rodata
.table:
dq .case_0
dq .case_1
dq .case_2
dq .case_3
Calls and returns#
call pushes the return address (the instruction after the
call) on the stack and jumps. ret pops it back into rip.
call helper ; push rip; jmp helper
; on return, rip is here
helper:
; ...
ret ; pop rip
See Functions for the prologue / epilogue conventions and calling-convention details.
ARM64 flow#
ARM64 uses a single condition-code field; one compare, multiple conditional branches.
cmp x0, #10
b.lt .less // signed less
b.lo .below // unsigned less (lower)
b.eq .equal
// unconditional
b .label
bl function // branch-and-link (=call); writes lr
ret // x30 (lr) -> pc
ARM64 also has conditional select (csel) and
conditional set (cset) for branch-free patterns.
cmp x0, #0
csel x1, x2, x3, lt // x1 = (x0 < 0) ? x2 : x3
SETcc and CMOVcc (x86-64)#
Branch-free patterns: setcc writes 0 or 1 to a byte
register based on a flag; cmovcc conditionally moves.
cmp rdi, 0
setl al ; al = (rdi < 0) ? 1 : 0
movzx eax, al
cmovl rax, rbx ; if (last cmp was signed less) rax = rbx
Reach for these to avoid branch mispredicts in hot loops.
Indirect branches#
Jumping through a register or memory cell. Used for vtables, jump tables, function pointers, and exploit payloads.
call rax ; call the function whose address is in rax
jmp qword [rdi + 8] ; tail call via a function-pointer table