Every processor has one register that is not like the others. It holds the address of the next instruction to fetch, and the machine cannot execute anything without consulting it. It is called the program counter on most architectures and the instruction pointer on Intel’s – IP on the 8086, EIP once the registers widened to 32 bits, RIP at 64 – and it is register 15 in the ARM register file, though it is not quite an ordinary register there.
The fetch cycle is what makes it necessary. The processor reads the word at the address in the PC, advances the PC by the length of the instruction it just read, and executes. Instructions on x86 are variable-length, so that increment is a consequence of decoding: a nop advances by one, a mov with a 32-bit displacement by six or seven. On fixed-width machines the increment is constant, which is a large part of why they pipeline better.
When reading the PC does not give you the PC
ARM reads of r15 return the address of the current instruction plus eight in ARM state, plus four in Thumb. Nothing is wrong with the register; the value is a fossil of the three-stage pipeline, where the PC had already been incremented twice by the time an instruction could read it. The behaviour was documented, depended upon, and preserved across several revisions of the architecture. In AArch64 the PC stopped being a general-purpose register at all, and that entire class of confusion disappeared with it.
AVR is stranger still. Its program counter is word-addressed rather than byte-addressed, because most instructions are 16 bits wide and there is no reason to spend an address bit distinguishing even addresses. A byte address in flash is therefore twice the PC. More awkwardly, the PC is not mapped into the data address space at all: there is no instruction that loads it into a register. To find out where you are, you call a subroutine and read the return address it pushed onto the stack – which is a byte address, so the conversion is explicit and easy to get wrong.
Relative branches, and why they matter more than they look
Almost every branch is encoded relative to the PC rather than as an absolute target, and this is not only a space saving. A relative branch is position-independent: the same instruction works wherever the code is loaded, which is what lets a shared library be mapped at whatever address the loader picks – and, on a modern system, at a randomised one.
x86-64 introduced RIP-relative addressing as the default way to reach a global. The reason is arithmetic: a full 64-bit absolute address does not fit in an instruction, and the alternatives are an eight-byte immediate load followed by an indirect access, or a displacement from a fixed base register. mov rax, [rip+0x1a2b] costs one instruction and reaches anything within two gigabytes of the current position, which covers essentially every global in a normal program. It also means the code contains no absolute addresses at all, so address-space layout randomisation gets its entropy for free.
Branch delay slots
On MIPS and SPARC the instruction immediately after a branch executes whether or not the branch is taken. The PC had already been incremented by the time the processor knew a branch was pending, so the following instruction had already been fetched; rather than waste the slot, the architecture defined it as always executing. Compilers fill it with useful work when they can and with a nop when they cannot, which is why disassembled MIPS is peppered with no-ops after jumps. It saves a cycle on paper and costs a great deal of pipeline complexity, which is why later architectures dropped it.
Interrupts and resets
An interrupt is, at the level of the PC, a forced call. The processor pushes the current PC – along with the status register, so the interrupted code’s condition flags survive – loads a new PC from a vector table, and jumps. The handler ends with reti, which pops the saved status and PC back. Everything an RTOS does about context switching rests on this: the saved PC on the stack is the suspended thread’s position, so switching threads is a matter of swapping stacks.
Where the vector table lives is architecture-specific. On AVR it sits at address zero in flash, one jump instruction per interrupt source, so an interrupt costs a jump through a table that is literally executable code. On Cortex-M the table is a list of addresses at the start of the address space, with the first entry reserved for the initial stack pointer and the second for the reset vector – which is to say that power-on is not special-cased at all. It is the reset exception, and the PC simply loads from the second word of the table. On x86 the reset vector is at the very top of the address space, 0xFFFFFFF0, a location chosen so the first fetch is reachable before any memory controller configuration has happened.
The PC as a target
The register is also the thing an exploit wants. Overwriting a saved return address on the stack is precisely an attempt to control the PC at the moment of a ret, and the whole defensive apparatus – non-executable stacks, canaries, control-flow integrity – exists to make that harder. The same mechanism serves debugging. A software breakpoint is an instruction patched to int3, whose handler restores the original byte and adjusts the saved PC so that execution resumes in the right place; single-stepping is a hardware flag that raises a trap after every instruction, which is to say after every PC update.