Skip to content

08 — Control Flow

Control flow directs the CPU to execute instructions in non-sequential order. In assembly, this is done explicitly with jumps. There are no if/else/for constructs — you build them yourself.


Unconditional Jump

jmp label    ; RIP = address of label (always)

Equivalent to goto in C. Used to implement loops and skip blocks.


Conditional Jumps

Conditional jumps check the flags register and jump only if the condition is met.

Jumps After CMP (most common)

cmp rax, rbx    ; sets flags based on rax - rbx
Instruction Condition Flags Checked C Equivalent
je / jz equal / zero ZF=1 ==
jne / jnz not equal / not zero ZF=0 !=
jl / jnge less than (signed) SF≠OF < (signed)
jle / jng less or equal (signed) ZF=1 or SF≠OF <= (signed)
jg / jnle greater than (signed) ZF=0 and SF=OF > (signed)
jge / jnl greater or equal (signed) SF=OF >= (signed)
jb / jnae below (unsigned) CF=1 < (unsigned)
jbe / jna below or equal (unsigned) CF=1 or ZF=1 <= (unsigned)
ja / jnbe above (unsigned) CF=0 and ZF=0 > (unsigned)
jae / jnb above or equal (unsigned) CF=0 >= (unsigned)

Other Flag-Based Jumps

Instruction Condition
js Sign flag set (result negative)
jns Sign flag clear
jo Overflow flag set
jno Overflow flag clear
jc Carry flag set
jnc Carry flag clear
jp Parity flag set (even parity)
jnp Parity flag clear

Implementing if / else

C Source

if (rax > 10) {
    rbx = 1;
} else {
    rbx = 0;
}

Assembly Translation

    cmp  rax, 10
    jle  .else          ; if rax <= 10, jump to else
.if:
    mov  rbx, 1
    jmp  .end
.else:
    mov  rbx, 0
.end:
    ; continue...

Branchless Alternative (CMOVcc)

    cmp   rax, 10
    mov   rbx, 0
    mov   rcx, 1
    cmovg rbx, rcx    ; if rax > 10 (greater), rbx = 1

Eliminates branch misprediction penalties.


Implementing Loops

While Loop

// C
while (rcx > 0) {
    rax += rbx;
    rcx--;
}
.while:
    cmp  rcx, 0
    jle  .endwhile       ; exit if rcx <= 0
    add  rax, rbx
    dec  rcx
    jmp  .while
.endwhile:

Do-While Loop (more natural in assembly)

// C
do {
    rax += rbx;
    rcx--;
} while (rcx > 0);
.loop:
    add  rax, rbx
    dec  rcx
    jg   .loop           ; if rcx > 0, loop again

One fewer comparison per iteration — prefer do-while when at least one iteration is guaranteed.

For Loop

// C: sum = 0; for (i = 0; i < 10; i++) sum += i;
int sum = 0;
for (int i = 0; i < 10; i++) sum += i;
    xor  rax, rax    ; sum = 0
    xor  rcx, rcx    ; i = 0
.for:
    cmp  rcx, 10
    jge  .endfor
    add  rax, rcx    ; sum += i
    inc  rcx
    jmp  .for
.endfor:

LOOP Instruction

A legacy x86 instruction that decrements RCX and jumps if not zero:

    mov  rcx, 10    ; iteration count
.loop:
    ; body
    loop .loop      ; rcx--; if rcx != 0, jump to .loop

LOOP is slow on modern CPUs (microcoded). Prefer explicit dec rcx; jnz .loop.


Implementing switch / case

// C
switch (rax) {
    case 0: /* do A */; break;
    case 1: /* do B */; break;
    case 2: /* do C */; break;
    default: /* do D */;
}

Chain of CMP/JE (small ranges)

    cmp  rax, 0
    je   .case0
    cmp  rax, 1
    je   .case1
    cmp  rax, 2
    je   .case2
    jmp  .default

.case0:
    ; do A
    jmp .end_switch
.case1:
    ; do B
    jmp .end_switch
.case2:
    ; do C
    jmp .end_switch
.default:
    ; do D
.end_switch:

Jump Table (dense ranges, compiler-preferred)

section .data
    jump_table dq case0, case1, case2    ; array of addresses

section .text
    ; Bounds check
    cmp  rax, 2
    ja   .default

    lea  rdx, [jump_table]
    mov  rdx, [rdx + rax*8]    ; load handler address
    jmp  rdx                    ; indirect jump

.case0: ; ...
.case1: ; ...
.case2: ; ...
.default: ; ...

Jump tables give O(1) dispatch regardless of case count — compilers generate these automatically for large, dense switch statements.


SETcc — Set Byte on Condition

Sets a byte register/memory to 0 or 1 based on a flag condition:

cmp  rax, rbx
sete al          ; al = 1 if rax == rbx, else 0
setl al          ; al = 1 if rax < rbx  (signed)
setb al          ; al = 1 if rax < rbx  (unsigned)

Useful for converting comparison results into integer booleans.


Label Naming Conventions

NASM labels follow these conventions:

_start:          ; global symbol (exported, no leading dot)
global _start

.loop:           ; local label (scope limited to surrounding global label)
.end:            ; another local label

myfunction:      ; non-exported function label

Local labels starting with . are only visible within the surrounding global label scope, preventing name clashes:

foo:
    .loop: jmp .loop    ; .loop inside foo

bar:
    .loop: jmp .loop    ; different .loop — no conflict

Complete Example: FizzBuzz (1–20)

; fizzbuzz.asm — print numbers 1-20 with FizzBuzz rules
; (simplified: just exits with the last classification in rax)
; 0 = FizzBuzz, 1 = Fizz, 2 = Buzz, 3 = number

section .text
global _start

_start:
    mov  r12, 1      ; counter = 1

.loop:
    cmp  r12, 20
    jg   .done

    mov  rax, r12
    xor  rdx, rdx
    mov  rbx, 15
    div  rbx
    test rdx, rdx
    jz   .fizzbuzz   ; divisible by 15 → FizzBuzz

    mov  rax, r12
    xor  rdx, rdx
    mov  rbx, 3
    div  rbx
    test rdx, rdx
    jz   .fizz       ; divisible by 3 → Fizz

    mov  rax, r12
    xor  rdx, rdx
    mov  rbx, 5
    div  rbx
    test rdx, rdx
    jz   .buzz       ; divisible by 5 → Buzz

    ; else: just a number
    jmp  .next

.fizzbuzz:
    jmp .next
.fizz:
    jmp .next
.buzz:
    jmp .next
.next:
    inc  r12
    jmp  .loop

.done:
    mov  rax, 60
    xor  rdi, rdi
    syscall

Key Takeaways

  • Set flags with CMP (comparison) or TEST (bitwise check), then branch with Jcc
  • Use signed jumps (jl, jg) for signed integers; unsigned jumps (jb, ja) for unsigned
  • Do-while loops are more natural in assembly (one less comparison per iteration)
  • CMOVcc avoids branches altogether — good for performance
  • SETcc converts a condition into a 0/1 byte value
  • Jump tables provide O(1) dispatch for switch-like patterns

Next: 09 — Stack and Procedures