I remember the exact moment condition flags finally clicked for me. I was stepping through a disassembled loop and noticed that a single CMP instruction was silently updating four different flags at once — and every branch after it was reading a different combination of them. That’s when I realized flags aren’t a single on/off switch; they’re a small dashboard of signals that the CPU maintains constantly, and learning to read that dashboard is what separates someone who can run assembly from someone who can read it fluently.
This post covers exactly how conditional flags get set, which instructions touch them, and how different architectures approach the problem.
What Are Conditional Flags?
Conditional flags (also called status flags or condition codes) are individual bits inside a special CPU register that record properties of the most recent arithmetic or logical operation: was the result zero, negative, did it overflow, did it require a carry/borrow?
These flags exist because the CPU’s ALU produces more information than just a numeric result — it also knows things like “this subtraction went negative” or “this addition overflowed the register width.” Rather than throwing that information away, the CPU stores it in flag bits so later instructions can branch based on it.
Where Flags Live
| Architecture | Register | Key Flags |
|---|---|---|
| x86 (32-bit) | EFLAGS | CF, PF, AF, ZF, SF, TF, IF, DF, OF |
| x86-64 | RFLAGS | Same as above, extended to 64-bit context |
| ARM (AArch32) | CPSR | N, Z, C, V |
| ARM (AArch64) | PSTATE (NZCV) | N, Z, C, V |
The Core Flags Explained
| Flag | Name | Set When |
|---|---|---|
| ZF / Z | Zero Flag | Result equals 0 |
| SF / N | Sign Flag / Negative | Most significant bit of result is 1 |
| CF / C | Carry Flag | Unsigned addition overflow or subtraction borrow |
| OF / V | Overflow Flag | Signed arithmetic overflow |
| PF | Parity Flag (x86 only) | Number of set bits in low byte is even |
| AF | Auxiliary Carry (x86 only) | Carry from bit 3 to bit 4 (BCD arithmetic) |
Which Instructions Set Flags?
This is the part that trips up most learners. Not every instruction touches the flags. Broadly:
x86/x86-64: Flags Update Implicitly
Most arithmetic and logical instructions update flags automatically as a side effect:
add eax, ebx ; sets CF, OF, SF, ZF, AF, PF
sub eax, ebx ; same set of flags
and eax, ebx ; sets SF, ZF, PF; clears CF and OF
or eax, ebx ; same as AND
xor eax, eax ; commonly used to zero a register AND set ZF
cmp eax, ebx ; like SUB but discards the result
test eax, ebx ; like AND but discards the result
inc eax ; sets SF, ZF, OF, AF, PF — but NOT CF (this is a famous gotcha)
dec eax ; same exception: CF is untouched
Data movement instructions (MOV, LEA, PUSH, POP) generally do not affect flags at all.
ARM: Flags Update Only When You Ask
ARM takes the opposite philosophy. Ordinary arithmetic instructions leave flags alone unless you append an S suffix, or use a dedicated comparison instruction:
ADD X0, X1, X2 ; flags NOT updated
ADDS X0, X1, X2 ; flags updated: N, Z, C, V
SUB X0, X1, X2 ; flags NOT updated
SUBS X0, X1, X2 ; flags updated
CMP X1, X2 ; equivalent to SUBS but discards result
CMN X1, X2 ; compare negative (adds and discards result)
TST X1, X2 ; AND and discard, updates N and Z
This design lets the compiler (or you) control exactly when flag-setting side effects happen, which reduces false dependencies between instructions in a pipeline and can improve performance in tight, flag-sensitive loops.
Step-by-Step: How the ALU Computes a Flag
Let’s trace through an actual subtraction to see how each flag gets derived, using 8-bit registers for simplicity: 5 - 3.
- The ALU computes
5 - 3 = 2in binary:00000101 - 00000011 = 00000010. - ZF: Is the result 0? No → ZF = 0.
- SF: Is bit 7 (MSB) of the result 1? No → SF = 0.
- CF: Did the subtraction require a borrow (i.e., was the unsigned minuend smaller than the subtrahend)? No → CF = 0.
- OF: Did signed overflow occur (operands had different signs and result sign doesn’t match)? No → OF = 0.
Now try 3 - 5 in 8-bit:
00000011 - 00000101requires a borrow → result (two’s complement) is11111110(-2).- ZF = 0 (result isn’t zero).
- SF = 1 (MSB is 1, result is negative).
- CF = 1 (borrow occurred — on x86, CF represents “borrow” for subtraction).
- OF = 0 (no signed overflow, since -2 fits fine in a signed byte).
Internal Flow Diagram
flowchart TD
A["Fetch instruction (e.g. SUB, ADD, CMP)"] --> B[Decode operands and opcode]
B --> C[ALU performs operation]
C --> D[ALU produces numeric result]
C --> E[ALU produces raw carry/borrow signal]
D --> F{Result == 0?}
F -->|Yes| G[Set ZF]
F -->|No| H[Clear ZF]
D --> I{MSB of result == 1?}
I -->|Yes| J[Set SF/N]
I -->|No| K[Clear SF/N]
E --> L{Carry/borrow occurred?}
L -->|Yes| M[Set CF/C]
L -->|No| N[Clear CF/C]
D --> O{Signed overflow detected?}
O -->|Yes| P[Set OF/V]
O -->|No| Q[Clear OF/V]
G & H & J & K & M & N & P & Q --> R[Flags register updated]
R --> S[Next conditional instruction reads flags]
Using Flags in Conditional Branches
Once flags are set, subsequent instructions consume them via condition codes.
x86-64
cmp eax, ebx
je labels_equal ; jump if ZF == 1
jne labels_not_equal ; jump if ZF == 0
jg a_greater ; jump if signed greater (ZF=0 and SF=OF)
jl a_less ; jump if signed less (SF != OF)
ja unsigned_greater ; jump if unsigned greater (CF=0 and ZF=0)
jb unsigned_less ; jump if unsigned less (CF=1)
ARM (AArch64)
cmp x0, x1
b.eq labels_equal ; branch if Z == 1
b.ne labels_not_equal
b.gt a_greater ; signed greater
b.lt a_less ; signed less
b.hi unsigned_greater ; unsigned higher
b.lo unsigned_lower ; unsigned lower
Notice the deliberate distinction between signed (g/l, gt/lt) and unsigned (a/b, hi/lo) condition codes — this is exactly why both the carry flag and overflow flag exist separately: one governs unsigned comparisons, the other governs signed ones.
Practical Use Cases
- Loop counters:
dec ecx/jnz looprelies on ZF being set automatically by the decrement. - Overflow-safe arithmetic: Checking OF/CF after addition in cryptographic or big-integer libraries to detect when to carry into the next limb of a multi-word number.
- Sorting/comparison routines: Using SF and OF together for correct signed comparisons in custom sort implementations written in assembly.
- Kernel-level context switching: Saving and restoring the flags register as part of a process’s saved CPU state.
Debugging Flags
In GDB:
(gdb) p $eflags
(gdb) info registers eflags
In x64dbg or WinDbg, the flags are shown as a labeled set (e.g., C P A Z S T I D O), and you can toggle them manually to test branch behavior without changing the underlying data — a powerful technique when you want to force a specific code path during debugging.
Comparison: x86 Implicit vs. ARM Explicit Flag Setting
| Aspect | x86/x86-64 | ARM |
|---|---|---|
| Default behavior | Most ALU ops set flags automatically | Flags set only with S suffix or CMP/TST |
| Code density | Slightly denser (no extra suffix needed) | Slightly more explicit, self-documenting |
| Pipeline implications | Can create flag dependencies between unrelated instructions | Reduces false dependencies, better for out-of-order scheduling |
| Learning curve | Easier at first, but hidden side effects can surprise beginners | Steeper at first, but very predictable once understood |
Common Mistakes
- Assuming
INC/DECupdate the carry flag on x86 — they don’t, by design, so they can be used inside multi-precision loops without disturbing an existing carry. - Forgetting the
Ssuffix in ARM and wondering why aB.EQnever triggers. - Confusing signed vs. unsigned branch mnemonics (
JGvs.JA, orB.GTvs.B.HI), leading to bugs when negative numbers are involved.
Best Practices
- Always check your target architecture’s manual for exactly which flags a given instruction affects — don’t assume.
- When writing performance-critical loops, reuse flags already set by an arithmetic instruction rather than inserting redundant comparisons.
- Use unsigned condition codes when working with pointers or unsigned counters, and signed condition codes for typical integer arithmetic.
FAQs
Do floating-point operations set the same flags? No. x86 floating-point (x87, SSE, AVX) uses a separate comparison mechanism (e.g., COMISS/UCOMISS set flags in a way compatible with integer branches, but the FPU status word itself is distinct).
Can I set flags manually without an arithmetic operation? Yes — x86 has STC/CLC (set/clear carry), CLD/STD (direction flag), and similar instructions for direct manipulation.
Why does ARM require explicit flag setting? It’s a RISC design philosophy: keep instruction behavior predictable and let the compiler decide exactly when the extra flag-computation hardware needs to activate, improving both clarity and performance.
Summary and Key Takeaways
- Conditional flags are bits that record properties (zero, sign, carry, overflow) of the last flag-affecting operation.
- x86/x86-64 sets flags implicitly on most arithmetic/logical instructions; ARM requires explicit opt-in via the
Ssuffix or dedicated instructions. - Understanding which instructions touch which flags is essential for correct branching, debugging, and optimization.
- Signed and unsigned comparisons rely on different combinations of flags — mixing them up is a classic source of bugs.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1, Chapter 3 (EFLAGS)
- AMD64 Architecture Programmer’s Manual, Volume 1
- Arm® Architecture Reference Manual for A-profile architecture, Condition Flags chapter
- GNU Assembler (GAS) documentation, condition code mnemonics reference