Worked examples — ARM architecture overview
Before the matrix, we must earn every symbol so nothing appears un-defined.

The scenario matrix
Every cell below is a distinct thing that can happen. The examples that follow each name their cell(s).
| # | Cell class | Concrete trigger | Example |
|---|---|---|---|
| A | Equal operands | a == b → Z=1 |
Ex 1 |
| B | Signed, positive result, no overflow | a > b, both small |
Ex 2 |
| C | Signed, negative result | a < b |
Ex 3 |
| D | Signed vs unsigned disagree | top-bit ("negative-looking") number | Ex 4 |
| E | Overflow flips the sign (limiting case) | INT_MAX + 1 |
Ex 5 |
| F | Zero / degenerate input | compare to #0; the always/never conditions |
Ex 6 |
| G | Carry semantics on subtract (HS/LO) |
borrow vs no-borrow | Ex 7 |
| H | Real-world word problem | clamp a sensor value, branch-free | Ex 8 |
| I | Exam twist | reorder that silently breaks flags | Ex 9 |
| J | LS/LE "or-equal" boundary |
operands exactly equal at the edge | Ex 10 |
Ex 1 — Cell A: operands equal (Z = 1)
Forecast: guess r2 before reading on — and guess whether N is lit.
- Compute
r0 - r1 = 42 - 42 = 0. Why this step?CMPis subtraction; the flags come from this hidden result. - Set flags from 0. Why?
Z=1(result is zero),N=0(0 is not negative), and for subtractionC = NOT borrow:42-42needs no borrow, soC=1.V=0(no signed overflow). Why care about C here? We don't forEQ, but you should always track all four — a later instruction might read them. MOVEQreadsZ==1→ true → runs, sor2 = 1. Why?EQis defined as exactlyZ==1, andMOVsimply drops the constant1intor2.MOVNEreadsZ==0→ false → becomesNOP. Why?NEis the logical opposite; only one of the pair ever fires, sor2is not overwritten with 0.
Verify: equal inputs must give the "equal" branch → r2 = 1. ✓ And exactly one of the EQ/NE pair executed, as required.
Ex 2 — Cell B: signed, positive, no overflow
Forecast: is 7 > 3 "GT-true"? What do N and V have to be?
r0 - r1 = 7 - 3 = 4. Why? The comparison's hidden subtraction.- Flags:
Z=0(4≠0),N=0(4 positive),V=0(no overflow),C=1(no borrow). Why V=0? Both numbers tiny, result fits easily. GTneedsZ==0 AND N==V. HereZ=0✓ andN=V=0✓ → fires. Why theN==Vtest instead of justN==0? Because overflow can lie about the sign;N==Vis the honest "is it really positive?" test. We'll see it earn its keep in Ex 5.r2 = 10 + 1 = 11. Why this step?ADDGTfired, so theADDruns normally: it takes the oldr2(10), adds the immediate1, and writes11back tor2. HadGTfailed, this whole line would have been aNOPandr2would stay10.
Verify: 7 > 3 is true → r2 incremented → 11. ✓
Ex 3 — Cell C: signed, negative result (N=1, V=0)
Forecast: 3 < 7 is true — but which flag combination proves "less than"?
r0 - r1 = 3 - 7 = -4. Why? Hidden subtraction again.- Flags: result
-4→N=1,Z=0,V=0(numbers small, no overflow).C=0because3-7needed a borrow (unsigned3 < 7). Why doesC=0here butC=1in Ex 2? Subtract setsC = NOT borrow; here a borrow happened. LTneedsN != V.N=1,V=0→ they differ → fires. WhyN != Vrather than plainN==1? Same reason as GT: only the combination survives overflow. When there's no overflow (V=0),LTcollapses to just "N=1", which matches intuition.r2 = 10 - 1 = 9. Why this step?SUBLTfired, so theSUBruns: it takes oldr2(10), subtracts the immediate1, and writes9back. IfLThad failed the line would be aNOPleavingr2 = 10.
Verify: 3 < 7 true → decrement → 9. ✓
Ex 4 — Cell D: signed vs unsigned DISAGREE
This is the cell everyone gets wrong. The same bits answer "greater" differently depending on whether you asked GT (signed) or HI (unsigned).

Forecast: 0xFFFFFFFF is the largest 32-bit pattern. Surely it's "greater"... or is it?
- Two readings of
0xFFFFFFFF. Why? Bits carry no sign by themselves. As unsigned it is4294967295(huge). As signed (top bit 1) it is-1(tiny). This is the whole point of cell D — and exactly the two dots in the figure above. - Hidden subtraction
r0 - r1:0xFFFFFFFF - 1 = 0xFFFFFFFE. As signed that is-2. Why this step?CMPis subtraction; we need its flags, so we must compute the discarded result first. - Flags: result top bit is 1 →
N=1.Z=0.V=0(signed-1 - 1 = -2fits fine, no overflow).C=1(unsigned4294967295 - 1needs no borrow). GT(Z==0 AND N==V):N=1,V=0→N != V→ fails. Sor2unchanged (stays 0). Why? Signed,-1 > 1is false.HI(C==1 AND Z==0):C=1,Z=0→ fires,r3 = 1. Why? Unsigned,4294967295 > 1is true.
Verify: signed says no (-1 > 1 false), unsigned says yes (4294967295 > 1 true) → r2 = 0, r3 = 1. ✓ Two flags, one number, opposite answers — exactly why ARM has both GT and HI.
Ex 5 — Cell E: overflow flips the sign (limiting case)
Forecast: INT_MAX + 1 — the number should be positive-ish, but the bits...
- Add:
0x7FFFFFFF + 1 = 0x80000000. Why note the bits?0x80000000has top bit 1 → reads as the most negative signed value,-2147483648. - Flags:
N=1(top bit set).Z=0.C=0(no unsigned carry out of bit 31). And cruciallyV=1: we added two positives and got a negative — the sign is impossible, so overflow is flagged. Why does hardware bother withV? Precisely so conditionals can survive this trap. GEneedsN == V. HereN=1,V=1→ equal → fires!r2 = 1. Why is this the "correct" answer? Mathematically2147483647 + 1 = 2147483648is positive. The raw bits look negative (N=1), butV=1says "the sign bit is lying," andN==Vun-lies it. Had we naively testedN==0, we'd have wrongly called it negative.
Verify: true math result is positive → GE fires → r2 = 1, and this is why GE uses N==V, not N==0. ✓
Ex 6 — Cell F: zero input and the always/never conditions
Forecast: how many of these fire?
r0 - 0 = 0. Why? Comparing to the degenerate input#0— the cheapest possible test, which is why ARM assembler often letsCMP r0, #0detect "is it zero / negative?" in one shot.- Flags:
Z=1,N=0,V=0,C=1(subtracting 0 borrows nothing). ADDEQ(Z==1) → fires,r1 = 6. Why?EQis exactlyZ==1; the hidden subtraction gave 0, so the flag is set and theADDruns, taking oldr1(5)+ 1 = 6.ADDAL—AL(ALways) is the default condition every plain instruction carries. It reads no flags, always runs.r1 = 6 + 10 = 16. Why doesALexist as a suffix at all? So the 4-bit condition field is never "empty" — an unconditional instruction just stores theALcode1110.- The
NV("never") code (1111) was the degenerate opposite ofAL: an instruction that never runs, a permanentNOP. Which versions? In the original 32-bit ARM ISA up to ARMv4,NVgenuinely meant "never execute". From ARMv5 onward ARM repurposed the1111code for unconditional special instructions (e.g.BLX,PLD), soNVno longer means "never" — do not use it as aNOPon modern cores.
Verify: EQ fires (+1) and AL fires (+10) → 5 + 1 + 10 = 16. ✓
Ex 7 — Cell G: carry semantics on subtraction (HS/LO)
Forecast: does the bigger-minus-smaller case set or clear C?
- Case (i):
10 - 3 = 7. Unsigned,10 >= 3, so no borrow needed. Why does that matter? Rule: subtraction setsC = NOT borrow. No borrow →C=1. - Case (ii):
3 - 10 = -7(bits0xFFFFFFF9). Unsigned3 < 10, so a borrow happened →C=0. - Interpretation via the table's
HS/LO: after aCMP,HS(C==1) means "a >= bunsigned";LO(C==0) means "a < bunsigned". So case (i) would make anHS-conditional fire and anLO-conditional NOP; case (ii) the reverse. Why is this the reverse of naive intuition? People expect "carry = something went wrong." For subtract it is deliberately inverted so thatHS/LOread cleanly as unsigned>=/<.
Verify: case (i) C=1 (no borrow, 10>=3, so HS fires); case (ii) C=0 (borrow, 3<10, so LO fires). ✓
Ex 8 — Cell H: real-world word problem (branch-free clamp)
Recall Quick recap: why "branch-free" is worth the effort
A branch (a jump like JLE) that the CPU predicts wrongly forces a pipeline flush — throwing away the partly-done instructions already in flight. The parent note estimated that cost as cycles wasted per branch. Here is the fraction of branches the predictor gets wrong (a pure probability between 0 and 1, so 0.15 = "wrong 15% of the time"), and is the number of pipeline-cycles thrown away on each misprediction (units: cycles; 40 is typical for a deep modern pipeline). Their product has units of cycles per branch — the average penalty. Conditional execution avoids the jump entirely (a failed instruction is just a 1-cycle NOP), so we dodge that penalty. Deeper pipeline mechanics live in 5.2.03-pipelining.
Forecast: for 137, which of the two MOVs fires?
- First
CMP r0, #0withr0 = 137:137 - 0 = 137, soN=0,V=0,Z=0. Why? Check the low edge first; the hidden subtraction gives the flagsMOVLTwill read. MOVLTneedsN != V;N=0=V→ equal → NOP.r0stays137. Why NOP?137is not< 0, so we must not floor it.- Second
CMP r0, #100:137 - 100 = 37, soZ=0,N=0,V=0. Why re-compare? We now test the high edge; the earlier flags are stale, so we must refresh them (this exact staleness bites us in Ex 9). MOVGTneedsZ==0 AND N==V;Z=0,N=V=0→ fires,r0 = 100. Why GT not GE?100itself is allowed, so we only cap when strictly greater than 100.
Verify: input 137 clamps to 100; and by construction the code has zero branches, so no pipeline flush penalty. ✓
Ex 9 — Cell I: exam twist (an instruction silently clobbers the flags)
Forecast: 9 > 2 is obviously true — surely MOVGT runs?
CMP r0, r1sets the flags we want:9 - 2 = 7→Z=0,N=0,V=0→GTwould be true. Why? Correct so far — this is exactly the flag state that makesGTfire.ADDS r4, r5, r6ends inS, so it overwrites N/Z/C/V. Why is that the trap? OnlyS-suffixed instructions (andCMP) touch flags — but this one does. TheCMPresult is now gone, replaced by flags from the unrelated addition.- New flags from
ADDSgiving0:Z=1,N=0,V=0. Why? A sum of0setsZ, just like any zero result. MOVGTneedsZ==0 AND N==V; nowZ=1→ fails → NOP.r3is not set. Bug! Why did it happen? The unrelatedADDSsat between theCMPand its dependent conditional and resetZ.- Fix: either move
ADDSafterMOVGT, or use plainADD(noS) so it leaves flags alone. Why doesADDvsADDSmatter so much? TheSbit (bit 20 in the encoding — see 5.1.07-ARM-instruction-encoding) is the only thing separating "update flags" from "leave them".
Verify: with the buggy code MOVGT does not fire (because Z=1 after ADDS 0), even though 9 > 2. ✓ The lesson: conditional execution depends on the last flag-setter, not on the last CMP.
Ex 10 — Cell J: the "or-equal" boundary (LS and LE)
Forecast: 5 <= 5 is true (the "equal" half). But do the flag formulas actually catch it?
r0 - r1 = 5 - 5 = 0. Why? The hidden subtraction — and it lands exactly on the boundary case where equality matters.- Flags:
Z=1(result 0),N=0,V=0,C=1(no borrow,5 >= 5). LSneedsC==0 OR Z==1. HereZ=1→ fires,r2 = 1. Why does theOR Z==1clause exist? Because at equalityC=1(which alone would say "higher"), so the formula needs the extraZ==1term to still count "equal" as<=. Without it,5 <= 5would wrongly fail.LEneedsZ==1 OR N!=V. HereZ=1→ fires,r3 = 1. Why theZ==1term again? At equalityN==V(so theN!=V"strictly less" part is false); theZ==1term is what rescues the "or-equal" case.
Verify: 5 <= 5 is true both signed and unsigned → both fire → r2 = 1, r3 = 1. ✓ The Z==1 clause in each formula is exactly the "or-equal" half doing its job at the boundary.
Recall Self-check
After CMP r0, r1 with r0=0xFFFFFFFF, r1=1: does MOVGT fire? ::: No — signed that's -1 > 1 = false (N != V, so GT fails).
After the same CMP, does MOVHI fire? ::: Yes — unsigned 4294967295 > 1 = true (C=1, Z=0).
INT_MAX + 1 via ADDS: which flags? ::: N=1, Z=0, C=0, V=1 — and GE correctly fires because N==V.
Subtraction sets C = ? ::: NOT borrow — so C=1 (i.e. HS) means unsigned a >= b.
At CMP 5,5, do LS and LE fire? ::: Yes — both because Z==1 supplies the "or-equal" half.
Which suffix is the default carried by every plain instruction? ::: AL (always), condition code 1110.
Does NV still mean "never"? ::: Only up to ARMv4; from ARMv5 the 1111 code was repurposed for unconditional special instructions.
What single thing decides whether an ADD updates the flags? ::: The S suffix (ADDS updates, ADD does not).
Where to go next: the bit-level home of the S bit and condition field is 5.1.07-ARM-instruction-encoding; why branch-free code helps is deepened in 5.2.03-pipelining; the energy angle links to 7.1.05-power-optimization and the density story to 6.3.02-cache-memory. Contrast the whole conditional-execution idea with 5.1.10-x86-architecture, which relies on branch prediction instead.