Registers, the Program Counter, and the Stack Pointer
What This Concept Is
A register is a named slot of storage inside the CPU. It holds one machine word, is accessed in a single cycle, and is where almost all real computation happens. The register file is tiny (typically 16-32 general-purpose registers in modern ISAs) but is the fastest storage the program can touch.
Three registers carry special meaning on every ISA:
- the program counter (
PC, calledRIPon x86_64,pcon ARM/RISC-V) holds the address of the next instruction to fetch - the stack pointer (
SP,%rsp,sp) holds the address of the top of the current call stack - a frame pointer or base pointer (
%rbp,fp) when present marks the base of the current function's stack frame
The rest of the registers are general-purpose, but the calling convention (the ABI) pins specific roles on them: which registers pass arguments, which return values, which are caller-saved, which are callee-saved.
Why It Matters Here
Reading assembly is impossible without knowing what the registers mean. More importantly: most optimizations the compiler does are about keeping hot values in registers instead of reloading them from memory. You will constantly ask, "Which registers hold what here, and where does this value spill when we run out of them?"
Concrete Example
On Linux/x86_64 (System V ABI):
Argument passing: rdi, rsi, rdx, rcx, r8, r9
Return value: rax
Caller-saved: rax, rcx, rdx, rsi, rdi, r8-r11
Callee-saved: rbx, rbp, r12-r15
Stack pointer: rsp (16-byte aligned at call boundary)
A simple C function:
long add3(long a, long b, long c) { return a + b + c; }
compiles (with -O1) to roughly:
add3:
lea (%rdi,%rsi,1), %rax # rax = a + b (lea is a fast add here)
add %rdx, %rax # rax += c
ret # jump to *rsp, rsp += 8
No memory traffic. Arguments a, b, c came in rdi, rsi, rdx; the return goes out in rax; ret pops the saved return address into PC.
For RISC-V:
add3:
add a0, a0, a1 # a0 += a1
add a0, a0, a2 # a0 += a2
ret # jr ra
Same story, different names.
Common Confusion / Misconception
Learners treat registers as a pool of scratch space shared by everyone. They are not. They are carved up by the ABI, and a violation is a bug the compiler cannot catch. If a function scribbles over %rbx without restoring it, the caller's data silently corrupts later. When you handwrite assembly or read it, you must respect who owns what across a call.
How To Use It
When you look at a function's disassembly:
- Find the prologue:
push %rbp; mov %rsp, %rbp; sub $N, %rsp(x86_64), oraddi sp, sp, -N; sd ra, ...(RISC-V). This is where the stack frame is set up. - Identify which registers hold the arguments by matching the ABI above.
- Track spills: a
mov %rdi, -0x8(%rbp)tells you the compiler pushed an argument to the stack so it could reuse the register. - Find the epilogue:
mov %rbp, %rsp; pop %rbp; ret.retalways reads the saved return address from(%rsp)and jumps.
Check Yourself
- What does the program counter hold, and what updates it?
- Why does pushing to the stack decrement
%rspon x86_64 instead of incrementing it? - What is the difference between a caller-saved and a callee-saved register?
- Why are registers fundamentally faster than L1 cache, even though both can serve a read in under a nanosecond?
Mini Drill or Application
For each situation, decide (a) which registers would hold what, and (b) whether the function needs a stack frame at all:
int sum(int a, int b) { return a + b; }on x86_64 Linuxlong mul_and_add(long *p, long k) { return *p * k + 7; }on x86_64 Linuxvoid fill(int *buf, int n) { for (int i = 0; i < n; i++) buf[i] = 0; }on RISC-V
Confirm your guesses in Compiler Explorer with -O1 and -O2 and note where -O2 eliminates the frame.
Read This Only If Stuck
- Computer Organization and Design: 2.3 Operands of the Computer Hardware
- Computer Organization and Design: 2.8 Supporting Procedures in Computer Hardware
- Computer Organization and Design: 2.8 Supporting Procedures (Part 2)
- Computer Organization and Design: 2.10 MIPS Addressing for 32-bit Immediates and Addresses
- CODE: Registers and Busses
- CODE: CPU Control Signals (Part 2)