I want to be upfront about something before diving in: the status register and the Program Status Word overlap conceptually, and in a lot of casual documentation the terms get used almost interchangeably. But there’s a useful, more precise distinction worth drawing out — the status register (or flags register, or condition code register) usually refers specifically to the condition-flag-holding part of the CPU’s state, the piece that records the result characteristics of the last arithmetic or logic operation. This post focuses squarely on that role: how the status register drives conditional logic, how it’s structured across architectures, and how to use it effectively and correctly in real assembly code.
The Core Job of the Status Register
Every time the ALU performs an arithmetic or logic operation, it doesn’t just produce a result — it also produces metadata about that result: was it zero? Negative? Did it overflow? Did a carry or borrow occur? The status register captures exactly that metadata in individual bits, and subsequent instructions — especially conditional branches — read those bits to decide what to do next.
Without a status register, every “if” in your program would require some entirely separate mechanism for comparison and branching bundled into a single monolithic instruction. Instead, most architectures split this into two clean steps: (1) an operation that sets flags, and (2) a separate instruction that reads those flags to decide on a jump or conditional action. That separation is what makes the status register so central to control flow.
The Standard Flags
| Flag | Common Name | Meaning |
|---|---|---|
| Z | Zero | Set when the result of an operation is exactly zero |
| C | Carry | Set on unsigned overflow (addition) or borrow (subtraction) |
| N / S | Negative / Sign | Set when the result’s most significant bit is 1 (interpreted as negative in two’s complement) |
| V / O | Overflow | Set when a signed arithmetic operation overflows the representable range |
| P | Parity | (x86 only) Set based on the parity of the low byte of the result |
| A | Auxiliary Carry | (x86 only) Set on carry/borrow between the low and high nibble — used for BCD arithmetic |
x86/x86-64: The Status Bits Within EFLAGS/RFLAGS
On x86, the status flags live inside the same register as the control flags (interrupt-enable, direction, trap) — collectively EFLAGS/RFLAGS, discussed more broadly in the Program Status Word context. For this post, the relevant subset is: CF, PF, AF, ZF, SF, OF.
mov eax, 10
cmp eax, 10 ; performs eax - 10 internally, discards result, sets flags
je are_equal ; jump if ZF is set
mov eax, 5
sub eax, 10 ; 5 - 10 underflows in unsigned terms
jc borrow_occurred ; jump if CF is set
mov eax, 0x7FFFFFFF
add eax, 1
jo signed_overflow ; jump if OF is set
Notice CMP doesn’t store a result anywhere — its entire job is to compute operand1 - operand2 purely to update the flags, leaving both operands untouched. This is the cleanest illustration of the status register’s purpose: producing decision-making metadata without side effects on your actual data.
Signed vs. Unsigned Comparisons — Where People Get Tripped Up
cmp eax, ebx
ja unsigned_above ; CF=0 and ZF=0 — unsigned "above"
jg signed_greater ; SF=OF and ZF=0 — signed "greater"
This is the single most common source of subtle bugs involving the status register: using JG/JL (signed) when you meant JA/JB (unsigned), or vice versa. The underlying flags being tested are genuinely different, so mixing them up produces correct-looking code that misbehaves only on specific edge-case values.
ARM: APSR Condition Flags and Conditional Execution
ARM’s condition flags (N, Z, C, V) live in the APSR (part of CPSR on classic ARM, part of xPSR on Cortex-M). ARM’s status register does something particularly interesting compared to x86: many ordinary data-processing instructions can update flags optionally (with an S suffix), and many instructions can be conditionally executed based on those flags — not just branches.
CMP R0, R1 ; sets N, Z, C, V based on R0 - R1
BEQ equal_label ; branch if Z set
ADDS R2, R3, R4 ; "S" suffix: ADD but also updates flags
BCS carry_set_label ; branch if carry set
MOVGT R5, #1 ; conditionally executed: only runs if "greater than" holds
MOVLE R5, #0 ; only runs if "less than or equal" holds
That last pair (MOVGT/MOVLE) is a great example of how a status register can eliminate a branch entirely — the CPU evaluates the condition and simply skips executing the instruction if the condition isn’t met, rather than jumping around it. This ties directly back to reducing branch misprediction risk in pipelined designs.
Internal Working Process
flowchart LR
A[Operand A] --> C[ALU]
B[Operand B] --> C
C -->|Result| D[Destination register<br/>or discarded, e.g. CMP/TST]
C -->|Flag bits: Z, C, N, V...| E[Status Register]
E --> F{Next instruction<br/>checks condition?}
F -- Conditional branch --> G[Branch taken or not]
F -- Conditional/predicated instr --> H[Instruction executes or is skipped]
F -- Unrelated instruction --> I[Flags simply persist unless overwritten]
Comparison Table: Status Register Behavior Across Architectures
| Aspect | x86/x86-64 | ARM |
|---|---|---|
| Register housing flags | EFLAGS / RFLAGS | APSR (within CPSR / xPSR) |
| Flags updated automatically? | Most arithmetic/logic instructions update flags by default | Only if instruction has S suffix (e.g., ADDS) — otherwise flags untouched |
| Comparison-only instruction | CMP (subtract, discard result) | CMP (subtract, discard result) |
| Test-bits-only instruction | TEST (AND, discard result) | TST (AND, discard result) |
| Conditional execution scope | Branches only (Jcc) | Branches (Bcc) and many data-processing instructions (ADDcc, MOVcc, etc.) |
| Signed vs unsigned condition codes | Separate mnemonics (JG vs JA) | Separate condition codes (GT vs HI) |
x86 advantage: flags update by default on nearly every arithmetic instruction, which is convenient and predictable. ARM advantage: optional flag updates (S suffix) let you avoid clobbering flags you still need from an earlier comparison, and conditional execution of ordinary instructions reduces branching overhead.
Practical Use Cases
- Loop counters:
DEC/JNZ(x86) orSUBS/BNE(ARM) patterns rely entirely on the zero flag to know when a loop should terminate. - Multi-word/big-number arithmetic: chaining
ADC/SBB(x86) orADCS/SBCS(ARM) across multiple words depends on the carry flag propagating correctly from one instruction to the next. - Bit-testing without full comparisons:
TEST/TSTlet you check specific bits (e.g., “is this flag bit set in this value?”) using an AND operation whose result is discarded, keeping the original value intact. - Saturating arithmetic in DSP code: ARM’s
Qflag specifically flags saturation events during signal-processing-oriented instructions.
Debugging and Performance Considerations
- Common mistake: inserting an instruction between a flag-setting operation and its conditional consumer that unintentionally clobbers the flags you needed. On x86 this is easy to do accidentally since most instructions touch flags by default; ARM’s optional
Ssuffix actually helps avoid this if used deliberately. - Debugging tip: step through in a debugger and watch the flags register directly (most debuggers display it) whenever a conditional branch behaves unexpectedly — it almost always traces back to a flag being set (or not) differently than assumed.
- Optimization tip: on ARM, favor conditional instruction execution over short conditional branches in tight, performance-critical loops when the branch would otherwise be poorly predicted — it can avoid the pipeline flush entirely.
- Optimization tip: on x86, be conscious that flag-reading instructions occasionally create a dependency chain that limits out-of-order execution; spacing out flag-setting and flag-consuming instructions can sometimes help the scheduler.
Best Practices
- Learn the precise flag semantics of any conditional instruction you use — don’t guess based on the mnemonic name alone.
- Use
CMP/TEST(orCMP/TSTon ARM) rather than manual subtraction/AND when you only care about the flags and want to avoid overwriting a value you still need. - On ARM, use the
Ssuffix deliberately — don’t append it out of habit if you don’t actually need updated flags, since it can create false dependencies for the scheduler. - Be explicit about signed vs. unsigned intent in every comparison; choose
JG/JL/GT/LTorJA/JB/HI/LOdeliberately, not by pattern-matching from other code.
FAQs
Is the status register the same thing as the Program Status Word? They’re closely related — the status register (condition flags) is typically a subset of the broader Program Status Word, which also includes control bits like interrupt-enable and processor mode.
Do all instructions modify the status register? No. Data-movement instructions like MOV typically leave flags untouched on both x86 and ARM. On ARM, only instructions with the S suffix update flags at all; on x86, most arithmetic/logical instructions do, but explicit exceptions exist (check your ISA reference).
Why does ARM let you conditionally execute an ADD instead of just branching? It’s a deliberate pipeline-friendly design choice — evaluating a condition and skipping one instruction is cheaper and more predictable than a branch that might be mispredicted, especially for short, simple conditional operations.
Can I read the status register’s raw value directly? Yes — PUSHF/POPF and LAHF/SAHF on x86, and MRS/MSR on ARM, let you read and write the flags directly as ordinary register values when you need fine-grained control.
Summary and Key Takeaways
The status register is the part of the CPU’s state dedicated to recording characteristics of the last operation’s result — zero, carry, sign, and overflow, primarily — and it’s the mechanism that every conditional branch and much of arithmetic chaining depends on. It’s tightly related to, and often physically part of, the broader Program Status Word, but its specific job is narrower and more mechanical: capture result metadata, let subsequent instructions act on it.
Key takeaways:
CMP/TEST(x86) andCMP/TST(ARM) exist purely to set flags without altering operands.- Signed and unsigned comparisons rely on genuinely different flag combinations — mixing them up is a classic, subtle bug.
- ARM’s optional flag updates and conditional instruction execution offer finer control and can reduce branching overhead compared to x86’s default-on flag updates.
- Always verify flag semantics per instruction rather than assuming based on similar-looking mnemonics.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1 — EFLAGS and condition codes.
- AMD64 Architecture Programmer’s Manual, Volume 1 — Status flag definitions.
- ARM Architecture Reference Manual — APSR condition flags and conditional execution encoding.
- GNU Assembler (GAS) documentation — condition code mnemonics for x86 (
Jcc) and ARM (Bcc, suffix conditions).
