How Are Conditional Flags Set in Assembly Language? A Deep Dive

How are conditional flags set in Assembly language

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

ArchitectureRegisterKey Flags
x86 (32-bit)EFLAGSCF, PF, AF, ZF, SF, TF, IF, DF, OF
x86-64RFLAGSSame as above, extended to 64-bit context
ARM (AArch32)CPSRN, Z, C, V
ARM (AArch64)PSTATE (NZCV)N, Z, C, V

The Core Flags Explained

FlagNameSet When
ZF / ZZero FlagResult equals 0
SF / NSign Flag / NegativeMost significant bit of result is 1
CF / CCarry FlagUnsigned addition overflow or subtraction borrow
OF / VOverflow FlagSigned arithmetic overflow
PFParity Flag (x86 only)Number of set bits in low byte is even
AFAuxiliary 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.

  1. The ALU computes 5 - 3 = 2 in binary: 00000101 - 00000011 = 00000010.
  2. ZF: Is the result 0? No → ZF = 0.
  3. SF: Is bit 7 (MSB) of the result 1? No → SF = 0.
  4. CF: Did the subtraction require a borrow (i.e., was the unsigned minuend smaller than the subtrahend)? No → CF = 0.
  5. 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:

  1. 00000011 - 00000101 requires a borrow → result (two’s complement) is 11111110 (-2).
  2. ZF = 0 (result isn’t zero).
  3. SF = 1 (MSB is 1, result is negative).
  4. CF = 1 (borrow occurred — on x86, CF represents “borrow” for subtraction).
  5. 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 loop relies 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

Aspectx86/x86-64ARM
Default behaviorMost ALU ops set flags automaticallyFlags set only with S suffix or CMP/TST
Code densitySlightly denser (no extra suffix needed)Slightly more explicit, self-documenting
Pipeline implicationsCan create flag dependencies between unrelated instructionsReduces false dependencies, better for out-of-order scheduling
Learning curveEasier at first, but hidden side effects can surprise beginnersSteeper at first, but very predictable once understood

Common Mistakes

  • Assuming INC/DEC update 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 S suffix in ARM and wondering why a B.EQ never triggers.
  • Confusing signed vs. unsigned branch mnemonics (JG vs. JA, or B.GT vs. 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 S suffix 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
Total
1
Shares

Leave a Reply

Previous Post
What is the significance of the link register in subroutine calls

What is the significance of the link register in subroutine calls

Next Post
Explain the purpose of the zero flag in Assembly language

The Purpose of the Zero Flag in Assembly Language: A Complete Guide

Related Posts