Control Flow in Assembly
Learn how Assembly handles control flow using comparison instructions, conditional jumps, and loop patterns in x86 NASM
In high-level languages, control flow is structured: if, else, while, for. In Assembly, none of these keywords exist. The processor only knows how to do two things with the instruction pointer: execute the next instruction, or jump somewhere else. Every conditional and every loop is built from comparisons, processor flags, and jump instructions.
That makes Assembly control flow simultaneously simpler and more error-prone than what you may be used to. Simpler, because there are only a handful of primitives. More error-prone, because nothing prevents you from jumping into the middle of a function, falling off the end of a loop, or forgetting that a system call clobbered your counter register.
This tutorial walks through how x86 assembly expresses the building blocks of control flow: the cmp instruction, the flags it sets, the conditional jumps that read those flags, and the looping idioms built from them.
Comparison and Flags
The CPU has a special register called EFLAGS that stores result bits from arithmetic and comparison operations. The two flags that matter most for control flow are:
| Flag | Meaning |
|---|---|
| ZF (Zero Flag) | Set when the result of an operation is zero |
| SF (Sign Flag) | Set when the result is negative (high bit set) |
The cmp a, b instruction performs a - b internally, sets the flags based on the result, and discards the difference. You then use a conditional jump to read those flags and branch.
| Mnemonic | Jumps if… | Meaning |
|---|---|---|
je / jz | ZF = 1 | equal / zero |
jne / jnz | ZF = 0 | not equal / not zero |
jl | SF ≠ OF | signed less than |
jg | ZF = 0 and SF = OF | signed greater than |
jle | ZF = 1 or SF ≠ OF | signed less or equal |
jge | SF = OF | signed greater or equal |
jmp | always | unconditional jump |
A Complete Control Flow Example
The program below demonstrates an if/else if/else chain followed by a counted loop. Change the test number near the top to see different branches taken.
Create a file named control_flow.asm:
| |
What the Code Does
The conditional block uses two comparisons against zero. jg takes the positive branch; if that fails, jl takes the negative branch; if both fail, execution falls through to the zero case. Each branch ends with jmp .after_if so the code rejoins a single path — Assembly has no else keyword, only jumps that simulate one.
The loop is built from three pieces: an initialization (mov ecx, 3), a body that does the work, and a tail that decrements the counter and conditionally jumps back to the top. Because int 0x80 clobbers ecx, the loop pushes and pops the counter around the system call. Forgetting that detail is one of the most common bugs when first writing assembly loops.
Local Labels
Labels that begin with a dot (.positive, .loop_start) are local to the nearest non-local label above them — in this case _start. This lets you reuse short names like .loop_start in different functions without collisions.
Running with Docker
| |
Expected Output
Number is positive
Loop iteration
Loop iteration
Loop iteration
Done
Loop Patterns at a Glance
The example above shows the most common idiom: decrement-and-jump-if-nonzero. A few other patterns you will see in real code:
- While loop — test at the top with
cmpandjge/jle/etc., thenjmpback at the bottom. - Do-while loop — body first, then a single
cmp+ conditional jump at the bottom. One instruction shorter than a while loop, but the body always runs at least once. loopinstruction — x86 has a dedicatedloop labelinstruction that decrementsecxand jumps if non-zero in a single opcode. It is shorter to write but, on modern CPUs, often slower than the explicitdec+jnzpair used above.
Key Concepts
cmpsets flags, it doesn’t branch. The branch happens on the next conditional jump instruction, which reads those flags.- Conditional jumps come in signed and unsigned flavors.
jg/jlare signed;ja/jbare unsigned. Using the wrong one on negative numbers silently produces wrong results. - Fall-through is a tool, not a bug. Letting one branch fall through to the next saves a
jmpand is idiomatic when the branches share trailing code. - System calls clobber registers. On Linux 32-bit,
int 0x80does not preserveecx,edx, etc. Save anything you need around the call withpush/pop. - Labels structure your code, not the assembler’s output. A label is just a name for an address; there is no enforced block scope. Discipline replaces syntax.
- Local labels (
.name) prevent collisions. They are scoped to the enclosing global label, so short, descriptive names can be reused freely across functions. dec+jnzis the canonical counted loop. It naturally counts down to zero, wherejnzexits — no separate comparison needed.
Running Today
All examples can be run using Docker:
docker pull esolang/x86asm-nasm:latest
Comments
Loading comments...
Leave a Comment