4.1.5 · D3Computer Architecture (Deep)

Worked examples — Registers — general purpose, special (PC, SP, LR, CPSR)

3,649 words17 min readBack to topic

Before we touch a single hex digit, three reminders you must not skip:


The scenario matrix

Every cell below is a distinct situation this topic can throw at you. The eight worked examples that follow are each tagged with the cell(s) they cover, so that when you finish, no cell is left dark.

# Cell (the case class) Register(s) exercised Twist it tests
A Forward flow, no branch PC plain PC + L increment
B Backward relative branch (negative offset) PC signed loop-back, the "PC reads +8" gotcha
C Balanced push then pop (non-degenerate) SP full-descending order, self-cleaning
D Degenerate: pop an empty stack SP limiting/error case, underflow
E Nested call (LR would be destroyed) LR, SP, PC the overwrite trap + the PUSH {LR} fix
F Flags: pos+pos overflow (V=1, N=1, C=0) CPSR one sign combo
G Flags: neg+neg overflow (V=1, N=0, C=1) CPSR the other sign combo
H Flags: no overflow, but carry out (C=1, V=0) CPSR unsigned carry ≠ signed overflow
I Flags: exact zero result CPSR Z=1 boundary, and CMP a,a
J Real-world word problem SP, LR a recursive function's stack depth

We cover A–J across Examples 1–8. Watch the tags.


Example 1 — Plain forward fetch · cell A

Forecast: guess a number before reading on. Is it 0x8000, 0x8004, or something bigger?

First, name the two different PC values so we never confuse them:

  • = the address of the instruction we are talking about = 0x8000.
  • = the address of the default following instruction after the auto-increment.
  • = the integer value an executing instruction sees if it names PC as an operand.
  1. The hardware auto-increments PC at fetch time. Why this step? From the parent note: the CPU's only record of "where am I" is PC, and it advances before the instruction runs so the default next instruction is already lined up:
  2. Account for the pipeline read-back quirk. Why this step? On classic ARM, fetch is two stages ahead of execute. The crucial subtlety: this +8 is measured from (the original 0x8000), not from the already-incremented . The pipeline runs two instruction-slots ahead of the executing one, and two slots × = bytes, so: Why add to and not ? Because "two stages ahead" is defined relative to the instruction currently executing (0x8000), and is only one stage ahead. Adding 8 to 0x8004 would double-count that first step.

Verify: two different questions, two answers. "Where is the next fetch?" → 0x8004. "What integer does the running instruction see if it reads PC?" → 0x8008. Both are plus a multiple of 4, which must be true for word-aligned code. ✔ See the fetch loop in Instruction Cycle (Fetch-Decode-Execute); the two-ahead gap is a Pipelining artefact.

Figure — Registers — general purpose, special (PC, SP, LR, CPSR)
Figure 1 (text description). Three boxes along an address axis: the left box 0x8000 is the running ADD; a cyan +4 arrow points to the middle box 0x8004, labelled "next fetch" (); an amber +4 (pipeline) arrow points on to the right box 0x8008, labelled "PC read value" (). An amber caption reminds: the running instruction sees PC = current + 8, i.e. two hops of 4 bytes from the original address, not one.


Example 2 — Backward relative branch · cell B

Forecast: the jump goes backwards, so is positive or negative? Roughly how many instructions?

  1. Fix the reference point. Why this step? The offset is not measured from the branch itself but from where PC reads ( from Example 1), which is branch address + 8:
  2. Set up the branch equation from the parent note. Why this step? A relative branch does . We want the landing to be 0x8000:
  3. Solve for . Why this step? Isolate the unknown: The negative sign is the whole point: it means "jump backwards", encoded in Two's Complement Arithmetic so the hardware adds it just like any other number.

Verify: plug back: . ✔ Landed exactly on the loop top. Sanity on magnitude: from 0x8018 down to 0x8000 is 24 bytes = 6 instructions — matches .

Figure — Registers — general purpose, special (PC, SP, LR, CPSR)
Figure 2 (text description). A vertical address ladder with 0x8000 loop top near the top, 0x8010 branch lower, and 0x8018 = PC_ref (+8) just below the branch (amber). An amber arrow sweeps upward from the 0x8018 reference dot to the 0x8000 target dot, annotated PC_ref + 4*(-6) = 0x8000 and -24 bytes = 6 instrs back, visualising why the offset is negative.


Example 3 — Balanced push then pop · cell C

Forecast: after the balanced pair, what is SP? (Trust the invariant, then prove it.)

  1. PUSH step 1 — make room: . Why first? If we stored before decrementing, we would overwrite whatever the current top holds.
  2. PUSH step 2 — store: . Why here? SP now points to a fresh, unused slot.
  3. POP step 1 — read: . Why read first? We must extract the data before declaring the slot free.
  4. POP step 2 — free: . Why after? Freeing then reading would read a slot we already gave away.

Verify: final = original. ✔ This is exactly why the parent calls stacks self-cleaning. And R1 = 0xAA, the very byte we pushed. ✔ See The Stack and Function Calls (Calling Conventions).

Figure — Registers — general purpose, special (PC, SP, LR, CPSR)
Figure 3 (text description). Three side-by-side snapshots of two stacked memory slots (0x2000 and 0x1FFC). Left: SP arrow points at empty 0x2000. Middle: after PUSH, SP has dropped to 0x1FFC and that slot is filled amber with 0xAA. Right: after POP, the slot is emptied and SP has climbed back to 0x2000. An amber caption states SP returns to 0x2000, proving the stack is self-cleaning.


Example 4 — Degenerate case: popping an empty stack · cell D

Forecast: does SP go up or down? Is any real data read?

  1. POP step 1 — read the slot SP points at: . Why a problem? Nothing was ever pushed here; this address belongs to whatever lives above the stack (another frame, or nothing). We read garbage.
  2. POP step 2 — free: . Why a problem? SP is now above its legal starting point — this is stack underflow. Every subsequent access is corrupt.

Verify: the number 0x2004 itself is the alarm. In a healthy full-descending stack, SP is never greater than its empty-value 0x2000; SP 0x2000 is definitionally impossible for valid data. So proves underflow. ✔ (Limiting/degenerate cell covered: the empty stack is the boundary where pop is undefined, mirroring how the parent's balanced-pair invariant only holds when pushes ≥ pops.)


Example 5 — Nested call: the LR overwrite trap and its fix · cell E

Forecast: after B finishes its own BL C, what does LR hold — A's return spot or C's?

  1. A calls B. Why this step? BL B performs and . Here . So (where A wants to resume).
  2. B calls C without saving LR. Why this destroys A's return? BL C at 0x300 overwrites LR again: . A's 0x104 is gone. If B now tries BX LR to return, it jumps to 0x304 — back into B's own body region, not A. Corruption. ✗
  3. The fix — B saves LR on the stack at entry. Why the stack and not another register? The stack (The Stack and Function Calls (Calling Conventions)) gives unlimited nesting; a single register cannot. B does PUSH {LR} first: , then it may BL C freely.
  4. B returns correctly. Why POP {PC}? On exit POP {PC} loads the saved 0x104 straight into PC in one instruction: . A resumes exactly where it wanted.

Verify: without the fix, the return target was 0x304 (wrong). With PUSH {LR}/POP {PC}, the recovered return target is the pushed value 0x104 (right). Two different numbers → proves the trap is real and the fix works. ✔

Figure — Registers — general purpose, special (PC, SP, LR, CPSR)
Figure 4 (text description). Three boxes A, B, C in a call chain. An amber arrow BL B: LR=0x104 runs A→B; a second amber arrow BL C: LR=0x304 (A lost!) runs B→C, showing LR being overwritten. Below, an amber-outlined box captioned "0x104 preserved on stack" with the note that the fix is B doing PUSH {LR}=0x104 on entry and POP {PC}=0x104 to return, restoring A's address.


Example 6 — Flags across all four sign combinations · cells F, G, H, I

This single example sweeps every interesting flag scenario the ALU can produce on an 8-bit add, so no combination is left untested. Recall the parent's flag definitions: N = result MSB, Z = result is zero, C = carry out of the top bit (unsigned overflow), V = signed overflow .

Forecast: guess which of the four raise the signed-overflow flag V, and which raise the carry flag C. They are not the same set — that is the lesson.

(F) 0x7F + 0x01 — pos + pos → overflow (cell F).

  1. Signed values: . Why note the true sum? , outside the fence → we predict V=1.
  2. Bit result: .
  3. Flags: N=1 (MSB 1, reads as ); Z=0; C=0 (no carry out of bit 7); V=1 (pos+pos gave a negative-looking result).

(G) 0x80 + 0xFF — neg + neg → overflow (cell G).

  1. Signed values: . Why? , below the fence → predict V=1.
  2. Bit result: ; keep low 8 bits ; the extra 1 is a carry out.
  3. Flags: N=0 (MSB 0, reads as ); Z=0; C=1 (carry out of bit 7); V=1 (neg+neg gave a positive-looking result). Note V and C are both 1 here.

(H) 0xFF + 0x02 — mixed signs, carry but no overflow (cell H).

  1. Signed values: . Why predict V=0? Operands have different signs, so signed overflow is impossible — the sum always stays in range.
  2. Bit result: ; low 8 bits ; carry out .
  3. Flags: N=0; Z=0; C=1 (carry out); V=0. This is the key contrast: C=1 but V=0 — an unsigned carry is not a signed overflow.

(I) exact zero result (cell I).

  1. Signed values: . Why interesting? It hits the Z boundary and equals what CMP 5, 5 produces ().
  2. Bit result: ; low 8 bits ; carry out .
  3. Flags: N=0; Z=1 (all zeros); C=1; V=0 (different signs → no signed overflow).

Verify: the four rows below are re-derived by machine in the verify block.

case a+b (hex) low byte N Z C V
F 0x7F+0x01 0x80 1 0 0 1
G 0x80+0xFF 0x7F 0 0 1 1
H 0xFF+0x02 0x01 0 0 1 0
I 0x05+0xFB 0x00 0 1 1 0

Sanity: V matches the parent's rule in all four; C (carry-out) and V agree only by accident, never by rule — exactly the trap cell H isolates. ✔


Example 7 — CMP then conditional branch · cell I (twist)

Forecast: equal operands — will BEQ jump?

  1. CMP computes and throws the result away, keeping flags. Why subtract? Equality of and is the same question as "is ?", which the Z flag answers for free.
  2. BEQ tests . Why this exact flag? BEQ is defined as "branch iff ". Since , the branch fires.

Verify: general rule from the parent: CMP a,b then BEQ branches iff . Here , so it must branch — consistent. ✔ (Had they differed, , , no branch.)


Example 8 — Word problem: recursion depth and stack cost · cell J

Forecast: how many frames are alive at the same time, and does SP come home to 0x8000?

  1. Count the simultaneously-live frames. Why this step? Each in-progress call keeps its {R4, LR} on the stack until it returns. At the deepest instant, fact(4), fact(3), fact(2), fact(1) are all mid-flight (none has returned yet) → 4 frames alive at once.
  2. Cost per frame = 8 bytes. Why 8? PUSH {R4, LR} stores two words; full-descending means each pushed word moves SP down by 4, so two words → down 8. (Order within PUSH doesn't change the net move.)
  3. (a) Deepest SP. Why subtract? The stack grows toward lower addresses, so four frames drop SP by bytes:
  4. (b) SP after everything returns. Why add back? Each of the four POP {R4, PC} exits moves SP up 8 bytes; four pops undo the four pushes:

Verify: (a) deepest , which is exactly 32 bytes below start — matches 4 balanced frames × 8 bytes each. (b) final = the starting empty value, confirming the self-cleaning invariant holds even through recursion. ✔ If SP had not returned to 0x8000, some call forgot a matching POP — the classic stack leak. (Depth-vs-cost is why deep recursion can overflow the stack region; see The Stack and Function Calls (Calling Conventions).)

Figure — Registers — general purpose, special (PC, SP, LR, CPSR)
Figure 5 (text description). Four stacked frame boxes, each labelled {R4,LR}, for fact(4) down to fact(1), with SP addresses on the right descending 0x7FF8, 0x7FF0, 0x7FE8, 0x7FE0. A cyan arrow marks "SP start 0x8000" at the top; an amber arrow marks "deepest 0x7FE0" at the bottom. An amber caption summarises: 4 frames × 8 bytes = 32 down, then 32 up → back to 0x8000.


Recall Quick self-test (click to reveal)

On classic ARM, an executing instruction that reads PC sees current + how many bytes? ::: +8 (fetch is two stages ahead; two words = 8 bytes), measured from the original address not the incremented one. Full-descending push order — decrement then store, or store then decrement? ::: Decrement SP first, then store (so you don't clobber the current top). 0x80 + 0xFF on 8 bits: what are C and V? ::: C=1 (carry out) and V=1 (neg+neg overflow). 0xFF + 0x02 on 8 bits: C and V? ::: C=1 but V=0 — carry is not overflow. Why must a non-leaf function PUSH {LR}? ::: Its own BL overwrites LR, destroying its caller's return address. After a balanced sequence of pushes and pops, SP is where? ::: Back at its original value (stacks are self-cleaning). fact(4) with 8-byte frames from SP=0x8000 — deepest SP? ::: 0x7FE0 (four frames × 8 bytes down).