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

ZF

Zero Flag. Set when the result of the last arithmetic / logic instruction was zero.

SF

Sign Flag. Set when the result’s high bit was 1 (negative for signed).

CF

Carry Flag. Set on unsigned overflow / borrow.

OF

Overflow Flag. Set on signed overflow.

PF

Parity Flag. Set when the low byte of the result has an even number of 1 bits.

DF

Direction Flag. cld clears (forward); std sets (backward); affects movs / stos / scas / cmps.

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

je / jz

jump if equal / zero (ZF=1)

jne / jnz

jump if not equal (ZF=0)

js / jns

jump if sign / not sign (SF=1 / SF=0)

jc / jnc

jump if carry / no carry (CF=1 / CF=0)

jo / jno

jump if overflow / no overflow

ja / jae

unsigned above / above-or-equal

jb / jbe

unsigned below / below-or-equal

jg / jge

signed greater / greater-or-equal

jl / jle

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

References#