Visual walkthrough — Registers — general purpose, special (PC, SP, LR, CPSR)
We assume only these plain facts, everything else is built below:
- Memory is a long row of numbered boxes. A "box number" is an address.
- A register is a labelled scratch box that lives inside the CPU (built in the parent note).
- Instructions are 4 bytes wide on our machine, so "the next instruction" is always 4 addresses further on.
Step 1 — The board: memory, registers, and the very first PC
WHAT. We lay out the whole world. On the right, main memory: a tall column of boxes, each with an address written beside it. Low addresses at the bottom, high addresses at the top — this matters in a moment. On the left, our four special registers as little labelled slots.
WHY start here. Every later step is just "which slot now points at which box". If you cannot see the boxes, you cannot see the pointing. A register that "holds an address" is nothing but an arrow into this column — so we draw it literally as an arrow.
PICTURE.
- — the Program Counter, holds an address. The arrow's tail is the register, its head lands on the box the CPU will fetch next.
- — that address, written in hex (base-16, the
0xjust says "read me as hex"). It is where our program's first instruction sits.
Right now the stack is empty, so points at the very top of the reserved stack region. holds garbage — nothing has called anything yet.
Step 2 — One fetch: PC increments before the instruction runs
WHAT. The CPU reads the box at , and — in the same beat, before executing it — advances by the instruction width .
WHY before, not after? Because the default meaning of "next" is baked into hardware: after grabbing instruction number , the natural next thing is instruction . The CPU commits to that guess immediately. If the instruction turns out to be a branch, it will overwrite the guess (Step 5). Doing the increment first means a plain non-branch instruction needs zero extra work to "move on" — it already moved.
PICTURE.
- — where we just fetched from ().
- — the instruction width in bytes; on our machine . It is a constant of the hardware, not something the program picks.
- — the new value . The arrow has slid up one box.
This is the fetch half of the Fetch-Decode-Execute loop. (The parent note's "PC reads as current + 8" gotcha comes from this happening twice over in a pipeline — two instructions are in flight ahead of the one executing. We ignore the pipeline here so the arrows stay clean; the logic is identical.)
Step 3 — BL helper: the Link Register catches the return address
WHAT. Our program reaches BL helper at . BL means Branch with Link. It does two things in one atomic move:
- Copies into .
- Overwrites with the address of
helper.
WHY save and not ? Look at the picture: already advanced in Step 2, so is the instruction after the call. That is exactly where we want to resume. If we saved the call's own address instead, returning would re-run the call forever — an infinite loop.
PICTURE.
- — the Link Register. The blue arrow now pins the box at : "come back to me."
- — the return address, snapshotted the instant before we leave.
- — the yellow arrow leaps down to
helper's first instruction. Execution has physically jumped.
Two arrows, one instruction. Hold this frame — the corruption in Step 4 lives entirely in what happens to that blue arrow.
Step 4 — The nested-call trap (degenerate case: trusting LR alone)
WHAT. Suppose helper itself does a BL deeper before it returns. That second BL runs the same two-step move — and its step 1 overwrites with helper's own return address.
WHY this destroys everything. is a single slot. It can remember exactly one return address at a time. The moment a second call happens, the first caller's return address is gone. When control finally tries to get back to our original program, no longer points at — it points wherever the innermost call put it. The return goes to the wrong place.
PICTURE.
- The crossed-out blue arrow is the destroyed return address.
- The pink arrow is the new, unwanted value clobbering it.
This is a genuine edge case, not a rare bug: every non-leaf function hits it. So the " handles all returns" model is only valid for a leaf function — one that calls nobody. We now build the fix, and to do that we need the stack.
Step 5 — PUSH {LR}: the stack rescues the return address
WHAT. Before helper makes any nested call, it copies into memory using the stack. Our machine uses a full-descending stack: it grows toward lower addresses, and points at the last item pushed.
Push of value :
- — move the top down first, into empty space.
- — write the value there.
WHY move before writing? If we wrote first and moved second, we would overwrite whatever currently sits at the top. Making room first guarantees we never clobber a live item. (This ordering is derived, not arbitrary — flip it and the stack corrupts.)
PICTURE.
- — the Stack Pointer; its arrow slides down one box (lower address) because the stack is full-descending.
- — the memory box now under , which just received the value .
- The saved return address is now safe in RAM. is free to be clobbered — because the truth lives on the stack.
Now the BL deeper from Step 4 can happen harmlessly: it trashes , but our real return address is asleep on the stack.
Step 6 — POP {PC}: return in a single move
WHAT. When helper is done, it pops the saved value straight into :
- — read the top of the stack.
- — free the slot afterwards.
WHY read before freeing, and why into directly? Read-before-free is the mirror of push: you must use the data before you declare the box reusable, or a stray interrupt could overwrite it. Popping into (instead of then BX LR) is the return — loading with the return address makes the CPU fetch from there next. One instruction does the whole job.
PICTURE.
- — the box holding , our long-saved return address.
- — the yellow arrow jumps back to : we have returned to exactly one instruction past the original call.
- — the arrow slides back up; the stack is exactly as it was before we pushed. Balanced push/pop leaves unchanged — this is why the stack is self-cleaning.
Step 7 — CPSR: the branch that decided whether to jump at all
WHAT. We rewind to a subtler question. Every jump above was unconditional. But real code says "return early if the input was zero." How does the CPU test a condition without recomputing it? The arithmetic leaves a fingerprint in the CPSR flags.
CMP a, b computes , throws away the numeric result, but keeps the flags. Then BEQ (branch-if-equal) tests only the flag.
WHY this works. exactly when . The zero-detector in the ALU raises precisely in that case. So BEQ — literally "branch iff " — branches iff , without ever storing the subtraction's result. The flag is the memory of the comparison.
PICTURE.
- — the Zero flag, one bit inside CPSR. Lit means "the last result was all zeros."
- The arrow from CPSR to the branch shows the flag being read, not written — branches consume flags, the ALU produces them.
The full flag set is ; the parent note derives each. Here the point is only the wiring: ALU → flags → conditional branch. That closes the loop of how a CPU decides where to go.
The one-picture summary
One frame holds the whole story: walks forward by each fetch; a BL splits into "save into " plus "jump"; a nested call would clobber , so we spill it to the stack via (grows down), and pop it back into to return; and flags let a branch choose its direction. Four registers, one dance.
Recall Feynman retelling — the whole walkthrough in plain words
Imagine a courier delivering messages along a numbered street. The courier only ever remembers one thing about the street: the house number they're heading to next — that's the PC, and after each delivery it just ticks up by one house.
Sometimes a message says "run an errand at house 500, then come back." Before sprinting off, the courier scribbles their current return address on a sticky note — that's the LR, "come back to 0x1008." Off they go.
But at house 500 there's another "run an errand" note. If the courier scribbles the new return address on the same sticky note, the old one is gone — they'll never find their way home. That's the nested-call trap.
The fix is a spike of receipts by the door — a stack, and SP is the pin marking the topmost receipt. Before running the second errand, the courier impales their sticky note onto the spike (push, growing the pile downward). Now the sticky note can be reused freely. When the errand's done, they lift the top receipt off the spike and read it as their next destination in one motion (pop into PC) — home again, spike back to how it was.
And the CPSR is a tiny lamp by the door: it lit up "zero" or "negative" the last time the courier did any sum. Some notes say "only bother going if the lamp shows zero." The courier just glances at the lamp — no re-adding — and decides. That glance is a conditional branch.
Reveal-yourself checks:
Why does PUSH decrement SP before writing?
Why save PC_next in LR rather than PC?
PC still holds the call instruction; resuming there would re-run the call forever. PC_next is the instruction after the call.What single fact makes a leaf function safe with LR alone?
BX LR runs.After a balanced push then pop, what is SP?
How does BEQ know two values were equal without storing the subtraction?
CMP set the Z flag to 1 iff ; BEQ reads only that flag.