4.1.5 · D4Computer Architecture (Deep)

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

2,716 words12 min readBack to topic

Throughout, assume ARM-style, 32-bit, word = 4 bytes, full-descending stack, instruction length bytes, unless a problem says otherwise.


Level 1 — Recognition

Exercise 1.1 — Name the register

For each job, which special register does it? Choose from PC, SP, LR, CPSR.

a. Holds the address of the next instruction to fetch. b. Holds the return address of a function call. c. Holds the condition flags (was the last result zero, negative…?). d. Points at the top of the stack.

Recall Solution 1.1

a. PC (Program Counter) — the CPU has no other memory of "where it is." b. LR (Link Register) — written by BL. c. CPSR (Current Program Status Register) — the NZCV scoreboard. d. SP (Stack Pointer).

What it looks like: PC "SaiLs Cleverly" — the mnemonic orders them exactly PC, SP, LR, CPSR.

Exercise 1.2 — GPR vs special

Classify each as general-purpose or special-purpose, and say who decides its meaning (compiler or hardware): R0, R7, PC, CPSR, SP.

Recall Solution 1.2
  • R0, R7general-purpose. The compiler decides what they hold (register allocation). No fixed meaning imposed by hardware.
  • PC, CPSR, SPspecial-purpose. The hardware reads/updates them (auto-increment, flag-setting, push/pop).

Level 2 — Application

Exercise 2.1 — Fetch loop arithmetic

The PC currently holds 0x8000. Instructions are bytes. No branch is taken. After fetching three instructions in sequence, what value is in PC (assuming the simple model where PC advances by per fetch)?

Recall Solution 2.1

What: each fetch does with . Why: the default next instruction sits one instruction-width higher in memory. ( in hex is 0x0C.) So PC 0x800C.

Exercise 2.2 — PUSH / POP trace

SP starts at 0x2000. Execute in order (full-descending, word = 4):

PUSH R0   ; R0 = 0x11
PUSH R1   ; R1 = 0x22
POP  R2

Give SP after each step, and the final value in R2.

Recall Solution 2.2

Rule (parent note): PUSH does then store; POP does read then .

Step Action SP after Memory written / read
start 0x2000
PUSH R0 ; Mem[0x1FFC]=0x11 0x1FFC store 0x11
PUSH R1 ; Mem[0x1FF8]=0x22 0x1FF8 store 0x22
POP R2 R2=Mem[0x1FF8]=0x22; 0x1FFC read 0x22

Final: SP 0x1FFC, R2 0x22. What it looks like: the last thing pushed (0x22) is the first thing popped — LIFO. SP did not return to 0x2000 because we pushed twice, popped once (unbalanced).

Exercise 2.3 — CMP then branch

An 8-bit CPU runs CMP R0, R1 with R0 = 0x30, R1 = 0x30, followed by BEQ label. Does the branch to label happen? Show the Z flag.

Recall Solution 2.3

What CMP does: computes , throws away the result, keeps the flags. What BEQ does: branches iff . Since , the branch is taken — control goes to label. Correct, because CMP a,b sets exactly when .


Level 3 — Analysis

Exercise 3.1 — The V flag

An 8-bit signed add computes 0x40 + 0x40. Give N, Z, C, V and explain V from first principles.

Figure — Registers — general purpose, special (PC, SP, LR, CPSR)
Recall Solution 3.1

0x40 (MSB → positive). True sum . Bit pattern: 0x80.

  • N: MSB of result is 1 → (result reads as ).
  • Z: result .
  • C: no carry out of bit 7 → .
  • V: signed-overflow rule . Here (both positive) but (result negative) → .

Why V=1 is the truth: two positives () cannot legitimately sum to a negative. The representable signed range is ; overflowed the top, so the sign bit lies. is the hardware raising its hand: "your signed math is wrong here."

Exercise 3.2 — Why C and V disagree

For the same 0x40 + 0x40, we found but . How can unsigned overflow (C) say "fine" while signed overflow (V) says "broken"? Argue from what each flag measures.

Recall Solution 3.2

What C measures: did the sum leave the unsigned range ? Unsigned, , which fits in . No carry out of the top bit → . Unsigned math is correct. What V measures: did the sum leave the signed range ? Signed, → overflow → . The resolution: the same 8 bits mean two different numbers depending on interpretation. 0x80 = unsigned (right) but signed (wrong). C polices the unsigned reading; V polices the signed reading. They are independent because the two interpretations have different range edges. This is why the ALU computes and stores both.

Exercise 3.3 — The nested-call bug

funcA:
  BL   funcB      ; (1)
  ...
  BX   LR         ; (2)  return from A
funcB:
  BX   LR         ; (3)  return from B

funcA was itself called from main via BL funcA. Trace LR and explain what goes wrong at line (2).

Recall Solution 3.3
  • main did BL funcA (address in main after the call). Call this M_ret.
  • Line (1) BL funcB (address in A after the call), call it A_ret. M_ret is now destroyed — LR is a single register.
  • Line (3) BX LR returns to A_ret. Fine so far.
  • Line (2) BX LR in A does . But LR still holds A_ret, not M_ret. So A "returns" to a point inside itself → infinite loop / corruption.

Root cause: BL overwrites the one LR. Any function that itself calls another (a non-leaf) must save LR before the inner BL. Fix: funcA must PUSH {LR} on entry and POP {PC} on exit (or POP {LR} then BX LR). The stack gives unlimited nesting; LR is only the fast top-of-frame for a leaf.


Level 4 — Synthesis

Exercise 4.1 — Leaf vs non-leaf prologue

You are hand-writing two functions:

  • leaf(x) — just returns x+1, calls nothing.
  • outer(x) — calls leaf twice, then returns the sum.

For each, decide whether you must PUSH {LR} at entry, and write the minimal prologue/epilogue. Justify each line.

Recall Solution 4.1

leaf: calls nothing → no BL executes inside it → LR is never overwritten. No need to save it.

leaf:
  ADD R0, R0, #1   ; R0 = x+1
  BX  LR           ; return; LR still holds caller's address

outer: contains BL leaf, which overwrites LR. It is non-leaf → must save LR. It also needs a value ( a running sum) to survive across the BL, so save a callee-saved register too.

outer:
  PUSH {R4, LR}    ; save LR (BL will clobber it) + R4 (must survive BL)
  BL   leaf        ; R0 = x+1 ; clobbers LR, R0-R3
  MOV  R4, R0      ; stash first result in callee-saved R4
  BL   leaf        ; clobbers LR again (harmless, it's on the stack)
  ADD  R0, R4, R0  ; sum of the two results
  POP  {R4, PC}    ; restore R4, pop saved-LR straight into PC = return

Why R4 not R1? R0–R3 are caller-saved scratch — the BL leaf is allowed to trash them. R4+ are callee-saved, so they survive across the call (as long as we restore them). Why POP {…, PC}? loading the saved return address directly into PC returns in a single instruction (efficient) — see The Stack and Function Calls (Calling Conventions).

Exercise 4.2 — Relative branch target

A relative branch sits at address 0x1000. The reference is 0x1008 (current + 8, the classic ARM pipeline offset). The signed offset is instructions, . Compute the branch target.

Figure — Registers — general purpose, special (PC, SP, LR, CPSR)
Recall Solution 4.2

Formula: . What it looks like: means "jump backward 3 instruction-slots from the reference point" — a loop back. 0x0C, and 0x1008 - 0x0C = 0x0FFC. Why is current+8 not current: by the time this instruction executes, the pipeline has already advanced PC two fetches ahead (). The assembler bakes the offset relative to that value.


Level 5 — Mastery

Exercise 5.1 — Design a signed-less-than test

You have only the ALU flags N, Z, C, V. After CMP a, b (which computes ), derive the boolean condition that means signed . Explain why alone is not enough.

Recall Solution 5.1

First attempt: signed , and "result negative" seems to be . But can overflow the signed range, flipping the sign bit — then lies. Fix using V: the true sign of is unless overflow occurred, in which case it's the opposite of . So the true "negative" condition is: Case check:

  • No overflow (): condition is → result genuinely negative. ✔
  • Overflow (): condition is → the sign bit reads positive but the true result was negative (it overflowed). ✔

This is exactly ARM's BLT (Branch if Less Than, signed): condition . This is why the ALU bothers to compute V.

Exercise 5.2 — Round-trip invariant

Prove that after a balanced sequence — any number of PUSHes each later matched by exactly one POP, properly nested — SP returns to its starting value , regardless of the values pushed.

Recall Solution 5.2

Model: each PUSH applies ; each POP applies . Data movement doesn't touch SP's value, only these steps do. Let there be PUSHes and POPs. Final SP: "Balanced" means every PUSH is matched by exactly one POP → : Why nesting matters for correctness (not for the SP value): the arithmetic above only needs for SP to return. But proper nesting (LIFO order) is what guarantees each POP reads back the right value — the one its matching PUSH stored. Un-nested pops would restore SP correctly yet return scrambled data. This is the self-cleaning property that makes the stack safe for function frames.

Exercise 5.3 — Why PC+8 breaks a naive assembler

On classic 3-stage ARM, reading PC gives current + 8. A student writes a branch as PC = PC + offset intending offset = target - current. Their loop lands 8 bytes past where they wanted. Explain the off-by-8 and give the corrected offset formula.

Recall Solution 5.3

What the student assumed: (when read) equals the address of the current instruction. So they set . What actually happens: because the pipeline fetches two stages ahead, reading yields . Their hardware computes: They overshoot by 8. Corrected offset: solve : Takeaway: always compute relative offsets against the pipeline-adjusted (), never the raw current address. This is why the parent note flagged PC+8 as "a famous gotcha."


Recall One-line self-test (reveal)

PUSH does :::: then store; POP does read then BEQ after CMP a,b branches iff :::: , i.e. Signed overflow flag rule :::: Signed less-than after CMP ::::