5.1.6 · D3Instruction Set Architecture (ISA)

Worked examples — ARM architecture overview

3,949 words18 min readBack to topic

Before the matrix, we must earn every symbol so nothing appears un-defined.

Figure — ARM architecture overview

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 == bZ=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.

  1. Compute r0 - r1 = 42 - 42 = 0. Why this step? CMP is subtraction; the flags come from this hidden result.
  2. Set flags from 0. Why? Z=1 (result is zero), N=0 (0 is not negative), and for subtraction C = NOT borrow: 42-42 needs no borrow, so C=1. V=0 (no signed overflow). Why care about C here? We don't for EQ, but you should always track all four — a later instruction might read them.
  3. MOVEQ reads Z==1 → true → runs, so r2 = 1. Why? EQ is defined as exactly Z==1, and MOV simply drops the constant 1 into r2.
  4. MOVNE reads Z==0 → false → becomes NOP. Why? NE is the logical opposite; only one of the pair ever fires, so r2 is 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?

  1. r0 - r1 = 7 - 3 = 4. Why? The comparison's hidden subtraction.
  2. 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.
  3. GT needs Z==0 AND N==V. Here Z=0 ✓ and N=V=0 ✓ → fires. Why the N==V test instead of just N==0? Because overflow can lie about the sign; N==V is the honest "is it really positive?" test. We'll see it earn its keep in Ex 5.
  4. r2 = 10 + 1 = 11. Why this step? ADDGT fired, so the ADD runs normally: it takes the old r2 (10), adds the immediate 1, and writes 11 back to r2. Had GT failed, this whole line would have been a NOP and r2 would stay 10.

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"?

  1. r0 - r1 = 3 - 7 = -4. Why? Hidden subtraction again.
  2. Flags: result -4N=1, Z=0, V=0 (numbers small, no overflow). C=0 because 3-7 needed a borrow (unsigned 3 < 7). Why does C=0 here but C=1 in Ex 2? Subtract sets C = NOT borrow; here a borrow happened.
  3. LT needs N != V. N=1, V=0 → they differ → fires. Why N != V rather than plain N==1? Same reason as GT: only the combination survives overflow. When there's no overflow (V=0), LT collapses to just "N=1", which matches intuition.
  4. r2 = 10 - 1 = 9. Why this step? SUBLT fired, so the SUB runs: it takes old r2 (10), subtracts the immediate 1, and writes 9 back. If LT had failed the line would be a NOP leaving r2 = 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).

Figure — ARM architecture overview

Forecast: 0xFFFFFFFF is the largest 32-bit pattern. Surely it's "greater"... or is it?

  1. Two readings of 0xFFFFFFFF. Why? Bits carry no sign by themselves. As unsigned it is 4294967295 (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.
  2. Hidden subtraction r0 - r1: 0xFFFFFFFF - 1 = 0xFFFFFFFE. As signed that is -2. Why this step? CMP is subtraction; we need its flags, so we must compute the discarded result first.
  3. Flags: result top bit is 1 → N=1. Z=0. V=0 (signed -1 - 1 = -2 fits fine, no overflow). C=1 (unsigned 4294967295 - 1 needs no borrow).
  4. GT (Z==0 AND N==V): N=1, V=0N != Vfails. So r2 unchanged (stays 0). Why? Signed, -1 > 1 is false.
  5. HI (C==1 AND Z==0): C=1, Z=0fires, r3 = 1. Why? Unsigned, 4294967295 > 1 is 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...

  1. Add: 0x7FFFFFFF + 1 = 0x80000000. Why note the bits? 0x80000000 has top bit 1 → reads as the most negative signed value, -2147483648.
  2. Flags: N=1 (top bit set). Z=0. C=0 (no unsigned carry out of bit 31). And crucially V=1: we added two positives and got a negative — the sign is impossible, so overflow is flagged. Why does hardware bother with V? Precisely so conditionals can survive this trap.
  3. GE needs N == V. Here N=1, V=1equal → fires! r2 = 1. Why is this the "correct" answer? Mathematically 2147483647 + 1 = 2147483648 is positive. The raw bits look negative (N=1), but V=1 says "the sign bit is lying," and N==V un-lies it. Had we naively tested N==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?

  1. r0 - 0 = 0. Why? Comparing to the degenerate input #0 — the cheapest possible test, which is why ARM assembler often lets CMP r0, #0 detect "is it zero / negative?" in one shot.
  2. Flags: Z=1, N=0, V=0, C=1 (subtracting 0 borrows nothing).
  3. ADDEQ (Z==1) → fires, r1 = 6. Why? EQ is exactly Z==1; the hidden subtraction gave 0, so the flag is set and the ADD runs, taking old r1 (5) + 1 = 6.
  4. ADDALAL (ALways) is the default condition every plain instruction carries. It reads no flags, always runs. r1 = 6 + 10 = 16. Why does AL exist as a suffix at all? So the 4-bit condition field is never "empty" — an unconditional instruction just stores the AL code 1110.
  5. The NV ("never") code (1111) was the degenerate opposite of AL: an instruction that never runs, a permanent NOP. Which versions? In the original 32-bit ARM ISA up to ARMv4, NV genuinely meant "never execute". From ARMv5 onward ARM repurposed the 1111 code for unconditional special instructions (e.g. BLX, PLD), so NV no longer means "never" — do not use it as a NOP on 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?

  1. Case (i): 10 - 3 = 7. Unsigned, 10 >= 3, so no borrow needed. Why does that matter? Rule: subtraction sets C = NOT borrow. No borrow → C=1.
  2. Case (ii): 3 - 10 = -7 (bits 0xFFFFFFF9). Unsigned 3 < 10, so a borrow happened → C=0.
  3. Interpretation via the table's HS/LO: after a CMP, HS (C==1) means "a >= b unsigned"; LO (C==0) means "a < b unsigned". So case (i) would make an HS-conditional fire and an LO-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 that HS/LO read 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?

  1. First CMP r0, #0 with r0 = 137: 137 - 0 = 137, so N=0, V=0, Z=0. Why? Check the low edge first; the hidden subtraction gives the flags MOVLT will read.
  2. MOVLT needs N != V; N=0=V → equal → NOP. r0 stays 137. Why NOP? 137 is not < 0, so we must not floor it.
  3. Second CMP r0, #100: 137 - 100 = 37, so Z=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).
  4. MOVGT needs Z==0 AND N==V; Z=0, N=V=0fires, r0 = 100. Why GT not GE? 100 itself 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?

  1. CMP r0, r1 sets the flags we want: 9 - 2 = 7Z=0, N=0, V=0GT would be true. Why? Correct so far — this is exactly the flag state that makes GT fire.
  2. ADDS r4, r5, r6 ends in S, so it overwrites N/Z/C/V. Why is that the trap? Only S-suffixed instructions (and CMP) touch flags — but this one does. The CMP result is now gone, replaced by flags from the unrelated addition.
  3. New flags from ADDS giving 0: Z=1, N=0, V=0. Why? A sum of 0 sets Z, just like any zero result.
  4. MOVGT needs Z==0 AND N==V; now Z=1fails → NOP. r3 is not set. Bug! Why did it happen? The unrelated ADDS sat between the CMP and its dependent conditional and reset Z.
  5. Fix: either move ADDS after MOVGT, or use plain ADD (no S) so it leaves flags alone. Why does ADD vs ADDS matter so much? The S bit (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?

  1. r0 - r1 = 5 - 5 = 0. Why? The hidden subtraction — and it lands exactly on the boundary case where equality matters.
  2. Flags: Z=1 (result 0), N=0, V=0, C=1 (no borrow, 5 >= 5).
  3. LS needs C==0 OR Z==1. Here Z=1fires, r2 = 1. Why does the OR Z==1 clause exist? Because at equality C=1 (which alone would say "higher"), so the formula needs the extra Z==1 term to still count "equal" as <=. Without it, 5 <= 5 would wrongly fail.
  4. LE needs Z==1 OR N!=V. Here Z=1fires, r3 = 1. Why the Z==1 term again? At equality N==V (so the N!=V "strictly less" part is false); the Z==1 term 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.