If there’s one register I underestimated the most when I started learning Assembly, it’s the condition code register. It doesn’t hold data you’re computing with directly, it doesn’t point to memory, and yet almost every conditional branch in every program you’ve ever run depends on it completely. Once I understood how flags get set and consumed, branching logic in Assembly stopped feeling like magic and started feeling like simple, predictable bit-testing. Here’s the full picture.
What Is the Condition Code Register?
The condition code register (called FLAGS/EFLAGS/RFLAGS on x86, and PSTATE/NZCV on ARM) is a special-purpose register whose individual bits record the outcome characteristics of the most recently executed arithmetic or logical instruction — things like “did this produce zero?”, “did this overflow?”, “did this produce a negative result?”, “did an unsigned carry occur?”
Conditional branch instructions (JE, JG, B.EQ, B.LT, and so on) don’t re-evaluate any expression themselves — they simply inspect specific bits of this register and decide whether to jump based on their state. This is precisely why the condition code register is sometimes called the unsung backbone of all decision-making in Assembly.
The x86 FLAGS Register
On x86/x86-64, the relevant flags live in the lower 16 bits of EFLAGS/RFLAGS. The most important ones for everyday programming are:
| Flag | Bit | Name | Meaning |
|---|---|---|---|
| CF | 0 | Carry Flag | Set if an unsigned arithmetic operation overflowed (carry out of the MSB) |
| PF | 2 | Parity Flag | Set if the low byte of the result has an even number of 1 bits |
| ZF | 6 | Zero Flag | Set if the result of an operation was exactly zero |
| SF | 7 | Sign Flag | Set to the most significant bit of the result (1 = negative in two’s complement) |
| OF | 11 | Overflow Flag | Set if a signed arithmetic operation overflowed |
Here’s a direct demonstration:
section .text
global _start
_start:
mov eax, 5
sub eax, 5 ; result is 0
jz is_zero ; ZF is set, so this jump is taken
jmp not_zero
is_zero:
; ... handle zero case ...
jmp done
not_zero:
; ... never reached in this example ...
done:
mov rax, 60
xor rdi, rdi
syscall
SUB doesn’t just compute eax - 5; it also silently updates ZF, SF, CF, and OF based on the result, and JZ (jump if zero) simply reads ZF afterward.
Signed vs Unsigned Comparisons: Why Both CF and OF Matter
This is the single most important practical distinction the condition code register enables, and it’s genuinely subtle. CMP internally performs a subtraction and sets flags exactly like SUB would, without storing the result. But which flags you check afterward depends entirely on whether you’re comparing signed or unsigned values:
| Comparison Type | Instruction Sequence | Flags Checked |
|---|---|---|
Unsigned “below” (<) | cmp a, b then jb | Checks CF |
Unsigned “above” (>) | cmp a, b then ja | Checks CF and ZF |
Signed “less than” (<) | cmp a, b then jl | Checks SF vs OF (must differ) |
Signed “greater than” (>) | cmp a, b then jg | Checks ZF, SF vs OF |
| Equal (either signedness) | cmp a, b then je/jz | Checks ZF only |
This is exactly why comparing a signed negative number against an unsigned large number produces wildly different jump behavior depending on whether you use JL/JG (signed) or JB/JA (unsigned) — the underlying CMP sets the same flags either way, but the jump instruction interprets those flags completely differently.
mov eax, -1 ; as unsigned, this is 0xFFFFFFFF, a huge number
cmp eax, 1
jl signed_less ; taken: -1 < 1 as signed numbers
jmp signed_not_less
; but...
mov eax, -1
cmp eax, 1
jb unsigned_below ; NOT taken: 0xFFFFFFFF is NOT below 1 as unsigned numbers
ARM’s Condition Flags: NZCV
ARM uses a very similarly-purposed but differently-named set of four condition flags, packed into the PSTATE register (commonly referred to together as NZCV):
| Flag | Meaning |
|---|---|
| N | Negative — set to the sign bit of the result |
| Z | Zero — set if the result was zero |
| C | Carry — set on unsigned overflow (or “no borrow” for subtraction) |
| V | Overflow — set on signed overflow |
A key architectural difference: on ARM, most instructions do not automatically update flags unless you explicitly append an S suffix (or use CMP/CMN, which always set flags). This is a deliberate design choice that gives ARM assembly finer control over when flags get modified:
subs w0, w1, w2 // "subs" - S suffix means update flags
b.eq equal_case // branch if Z flag is set
b.lt less_case // branch if signed less-than
sub w0, w1, w2 // plain "sub" - does NOT touch NZCV at all
This is genuinely useful: you can perform several arithmetic operations in a row without one of them accidentally clobbering flags you still needed from an earlier comparison — something x86 code has to work around more carefully since most x86 arithmetic instructions always touch flags.
Condition Code Suffixes: x86 vs ARM
| Condition | x86 Jump Suffix | ARM Branch Suffix | Meaning |
|---|---|---|---|
| Equal | JE / JZ | B.EQ | ZF=1 / Z=1 |
| Not Equal | JNE / JNZ | B.NE | ZF=0 / Z=0 |
| Unsigned less than | JB / JC | B.CC (or B.LO) | CF=1 / C=0 |
| Unsigned greater/equal | JAE / JNC | B.CS (or B.HS) | CF=0 / C=1 |
| Signed less than | JL | B.LT | SF≠OF / N≠V |
| Signed greater than | JG | B.GT | ZF=0 and SF=OF / Z=0 and N=V |
| Overflow | JO | B.VS | OF=1 / V=1 |
Internal Working: How a Comparison Updates and Is Consumed
sequenceDiagram
participant Inst as CMP/SUB Instruction
participant ALU as Arithmetic Logic Unit
participant Flags as Condition Code Register
participant Branch as Conditional Jump/Branch
Inst->>ALU: Execute subtraction (a - b), discard or keep result
ALU->>Flags: Update ZF, SF, CF, OF (x86) or N, Z, C, V (ARM)
Note over Flags: Flags now reflect outcome characteristics
Branch->>Flags: Read specific flag bit(s)
Flags-->>Branch: Return flag state
Branch->>Branch: Take jump if condition matches, else fall through
Practical Use Cases
- Loop control:
DEC/SUBScombined withJNZ/B.NEis the classic pattern for counted loops. - Boundary checking: validating array indices or buffer sizes relies entirely on comparison flags before allowing an access.
- Overflow detection in arithmetic-heavy code: cryptography and big-number libraries explicitly check
OF/CF/V/Cto detect and propagate carries across multi-word additions. - Compiler-generated code: every
if,while,for, andswitchstatement in a high-level language ultimately compiles down to a comparison instruction followed by a conditional branch reading these exact flags.
Debugging Condition Flags
GDB shows the full flags register directly, decoded into readable flag names:
info registers eflags
# Example output: eflags 0x246 [ PF ZF IF ]
p $eflags
On ARM targets:
info registers cpsr
# Shows N, Z, C, V bits among other PSTATE fields
Watching how these flags change instruction-by-instruction with stepi is one of the best ways to actually internalize which instructions touch which flags — I still do this whenever I’m unsure about a particular instruction’s side effects.
Common Mistakes
- Using signed jump conditions on unsigned comparisons (or vice versa) — this is by far the most common flags-related bug, and it can silently produce wrong branch behavior without any assembler error.
- Assuming a
MOVinstruction affects flags — it doesn’t, on both x86 and ARM (plainmov, notmovs); relying on stale flags from an earlier instruction after an interveningMOVis a subtle bug source. - Forgetting the
Ssuffix on ARM when you actually need flags updated (e.g., writingsubinstead ofsubsbefore a conditional branch), resulting in branches based on stale, unrelated flag values. - Not accounting for
OFin signed arithmetic — checking onlySFto determine “negative result” ignores the possibility that a signed overflow flipped the sign bit incorrectly.
Best Practices
- Always match your comparison instruction’s signedness assumption with the correct jump/branch mnemonic (
JL/JGfor signed,JB/JAfor unsigned on x86; same logic applies conceptually on ARM). - On ARM, be deliberate about the
Ssuffix — only add it to instructions whose flag-setting behavior you actually intend to rely on afterward. - When writing multi-word (extended-precision) arithmetic, use
ADC/SBB(x86) orADCS/SBCS(ARM) which explicitly incorporate the carry flag from a previous operation, rather than manually trying to track carries yourself. - Document non-obvious flag dependencies in your code comments, especially when several instructions execute between a flag-setting operation and the branch that consumes it.
TEST and CMP: Setting Flags Without Destroying Data
Two instructions deserve special attention because they exist purely to set flags without altering your actual data: TEST and CMP on x86, and their ARM equivalents TST and CMP. CMP a, b performs a - b internally and updates flags exactly as SUB would, but discards the numeric result, leaving both operands untouched. TEST a, b performs a bitwise AND and updates ZF/SF/PF based on that result, again without storing it anywhere:
mov eax, ebx
test eax, eax ; classic idiom: AND eax with itself, sets ZF if eax == 0
jz is_zero ; jump if eax was zero, without ever destroying eax's value
This test eax, eax idiom is extremely common in real-world and compiler-generated code specifically because it’s a slightly cheaper way to check “is this register zero?” than cmp eax, 0, while leaving eax completely intact for subsequent use. ARM’s equivalent pattern uses cmp:
cmp w0, #0
b.eq is_zero
Since ARM’s cmp (unlike x86’s cmp) is inherently a flag-setting-only instruction by nature (there’s no non-flag-setting variant), there’s no equivalent ambiguity to worry about on that side.
Conditional Moves: Branch-Free Logic Driven by Flags
Condition codes don’t only drive jumps — they can also drive conditional move instructions, which let you write branch-free code that avoids the CPU’s branch predictor entirely, often a meaningful performance win in tight, data-dependent loops. On x86, the CMOVcc family reads the same flags a Jcc instruction would, but instead of jumping, it conditionally copies a value:
mov eax, 10
mov ebx, 20
cmp eax, ebx
cmovl ecx, eax ; ecx = eax only if eax < ebx (SF != OF, i.e. the "L" condition)
cmovge edx, ebx ; edx = ebx only if eax >= ebx
ARM has an equally rich conditional execution model, historically even more powerful in 32-bit ARM (via the “IT” — If-Then — block mechanism allowing up to four instructions to be conditionally executed), and in AArch64 through dedicated conditional-select instructions like CSEL:
cmp w0, w1
csel w2, w0, w1, lt // w2 = (w0 < w1) ? w0 : w1 -- a branch-free "min" operation
This single csel instruction implements a full min/max style conditional entirely without any branch, reading the same NZCV flags a b.lt would consume, just applying the condition to a data movement instead of a jump.
Multi-Word Arithmetic: Carrying Flags Across Instructions
Flags aren’t only useful within a single comparison-then-branch pattern — they’re essential for implementing arithmetic on numbers wider than a single register, something that comes up constantly in cryptography and big-number libraries. Adding two 128-bit numbers stored across register pairs on x86-64 looks like this:
; Add two 128-bit numbers: (rdx:rax) + (rcx:rbx) -> (rdx:rax)
add rax, rbx ; add low 64 bits, sets CF if it overflowed
adc rdx, rcx ; add high 64 bits PLUS the carry flag from the previous ADD
ADC (“add with carry”) explicitly incorporates whatever CF was left set by the preceding ADD, letting a single carry propagate correctly across an arbitrarily long chain of word-sized additions. ARM’s equivalent, ADCS, works identically:
adds x0, x0, x2 // add low words, S suffix updates NZCV including carry
adc x1, x1, x3 // add high words plus carry-in from previous adds
This carry-chaining pattern is precisely how compilers and hand-written big-number libraries implement arbitrary-precision arithmetic using only fixed-width registers, and it’s entirely dependent on the condition code register faithfully carrying state from one instruction to the next.
Preserving Flags Across Function Calls and Interrupts
One last practical concern worth covering: flags are extremely cheap to accidentally clobber, since almost every arithmetic instruction touches them on x86. If you need to preserve the current flag state across a sequence of code that itself performs arithmetic — for example, inside an interrupt handler, or around a function call whose internals you don’t fully control — you need to explicitly save and restore them:
pushfq ; push RFLAGS onto the stack
call some_function ; function may freely modify flags internally
popfq ; restore the original RFLAGS state
This pattern is especially critical inside interrupt service routines, where the interrupted code’s flags must be restored exactly before returning control, or the interrupted program could take a completely wrong conditional branch immediately after resuming, purely because its flags were silently altered while it was suspended. ARM handles this differently at the architectural level: exception entry automatically saves the current PSTATE (including NZCV) into SPSR_ELx (Saved Program Status Register), and the ERET instruction restores it automatically when returning from the exception, removing the need for manual push/pop of flags in most exception-handling code, unlike x86 where IRET restores flags from the stack frame that was pushed at interrupt entry, but any nested arithmetic inside the handler itself still needs explicit PUSHF/POPF protection around any code segment where clobbering matters.
A Mental Model Worth Keeping
If there’s one thing I’d want a reader to walk away with, it’s this: the condition code register is not a place where you store values — it’s a place where the CPU records facts about the last operation. Every time you see a conditional jump or branch in a disassembly listing, ask yourself which specific fact it’s checking (zero? negative? overflow? carry?) and which earlier instruction most recently established that fact. Once that habit becomes automatic, reading unfamiliar disassembly — including compiler-generated code you didn’t write yourself — becomes dramatically easier, because the flags-then-branch pattern is genuinely one of the most repeated idioms in the entire history of computer architecture.
Frequently Asked Questions
Q: Do all instructions update the condition code register? No. On x86, most arithmetic and logical instructions do, but MOV, LEA, and several others explicitly do not. On ARM, only instructions with the S suffix (or dedicated comparison instructions like CMP/CMN) update flags — plain arithmetic instructions leave NZCV untouched.
Q: What’s the difference between CF and OF (or C and V on ARM)? CF/C tracks unsigned overflow (a carry or borrow out of the most significant bit), while OF/V tracks signed overflow (the result’s sign is mathematically wrong given the operands’ signs). They can, and often do, differ for the same operation, which is exactly why signed and unsigned comparisons use different flag combinations.
Q: Can I read or manually set the flags register directly? Yes, though it’s rare in typical application code. x86 has PUSHF/POPF to push/pop the flags register to/from the stack, and LAHF/SAHF for the lower byte specifically. ARM allows reading/writing PSTATE fields through specific system instructions, though direct manipulation is far less common in everyday ARM assembly than the automatic flag-setting mechanism.
Summary and Key Takeaways
The condition code register is the quiet mechanism underneath every if, loop condition, and comparison in both hand-written Assembly and compiler-generated machine code. On x86, this is the FLAGS/EFLAGS/RFLAGS register, with ZF, SF, CF, and OF being the flags you’ll interact with constantly. On ARM, the equivalent NZCV bits inside PSTATE serve the identical purpose, with the key architectural difference that ARM instructions only update flags when explicitly told to via the S suffix or dedicated comparison instructions.
Key points to remember:
- Conditional branches don’t evaluate expressions themselves — they read specific bits of the condition code register set by a prior instruction.
- Signed and unsigned comparisons require different jump/branch mnemonics even though the same underlying flags get set.
- ARM requires explicit opt-in (the
Ssuffix) for flag updates, unlike most x86 instructions which update flags by default. - Multi-word arithmetic relies on carry-aware instructions (
ADC/SBBon x86,ADCS/SBCSon ARM) that explicitly read the condition code register from a previous operation.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manuals, Volume 1: EFLAGS chapter — Intel Corporation
- AMD64 Architecture Programmer’s Manual — Advanced Micro Devices
- ARM Architecture Reference Manual for A-profile architecture, PSTATE and Condition Codes chapter — Arm Ltd.
- GNU Binutils and GNU Assembler (
as) Documentation — Free Software Foundation
