Skip to content
academia.sh

Lesson 09 / 16

Instructions and the Program Counter

The encoding of instructions as bit patterns, the fetch-decode-execute cycle, and how program flow arises.

Contents

The previous lesson established that data resides in memory as addressed bytes. Instructions reside in the same memory, in the same form: an instruction is nothing but a bit pattern encoded according to a defined rule. The distinction between program and data is not in the hardware but in the interpretation applied to those bytes.

This lesson asks: how does the processor turn a bit pattern in memory into an action, and how does it determine the next action?

The Instruction Set

Every processor family has a defined instruction set: a contract specifying which operations exist, which bit pattern encodes each one, and which operands each takes. The instruction set is the interface between hardware and software; two different processors that implement the same instruction set can run the same program.

The typical instruction classes are few:

  • Data movement — loading from memory into a register, storing from a register to memory.
  • Arithmetic and logic — addition, subtraction, comparison, bit operations.
  • Control flow — branching, subroutine call and return.
  • System — interrupts, changing privilege level, operating system calls.

Every construct in a programming language ultimately reduces to instructions from these classes.

How an Instruction Is Encoded

An instruction encoding is a pattern divided into bit fields. One field, the opcode, says which operation to perform; the others specify the operands.

To make the structure concrete, consider a 32-bit encoding defined purely for teaching purposes. This encoding does not belong to any real processor; field layout differs across architectures.

Bit range Field Width
31-24 opcode 8 bits
23-16 destination register 8 bits
15-8 first source 8 bits
7-0 second source 8 bits

If the course’s shared example is decoded under this rule:

0x414243440x41op 0x42dest 0x43src 1 0x44src 2\texttt{0x41424344} \rightarrow \underbrace{\texttt{0x41}}_{\text{op}}\ \underbrace{\texttt{0x42}}_{\text{dest}}\ \underbrace{\texttt{0x43}}_{\text{src 1}}\ \underbrace{\texttt{0x44}}_{\text{src 2}}

If opcode 0x41 is assumed to mean “add”, the instruction says: add the contents of registers 67 and 68, write the result to register 66.

The same thirty-two bits have now been interpreted four times over the course of this topic: as an integer, a floating-point number, text, and now an instruction. Which interpretation applies to the pattern is decided by where the processor reads those bytes from — as an instruction, or as data.

Fixed-length encodings (every instruction the same width) simplify the decode stage. Variable-length encodings instead shrink program size by fitting frequently used instructions into fewer bytes. Both approaches are common, and the choice reflects the architecture’s design priorities.

Machine Language and Assembly

Writing bit patterns by hand is error-prone. Assembly is a one-to-one representation that gives each instruction a readable name:

    load    r1, [1000]     ; load the value at address 1000 into r1
    load    r2, [1004]
    add     r3, r1, r2     ; r3 = r1 + r2
    store   [1008], r3

The relationship between assembly and machine language is nearly one-to-one: each line translates to one instruction. A line in a high-level language, by contrast, can correspond to dozens of instructions; that translation is performed by the compiler and is covered in this topic’s final lesson.

The Fetch-Decode-Execute Cycle

From the moment it powers on until it powers off, the processor repeats a single cycle:

  1. Fetch. The instruction is read from the address the program counter points to.
  2. Decode. The bit fields are parsed; the opcode and operands are determined.
  3. Execute. The operation is carried out by the arithmetic and logic unit or the memory unit.
  4. Write back. The produced value is written to the destination register or to memory.
  5. The program counter advances to the next instruction, and the cycle starts over.

The program counter is a special register that holds the address of the next instruction. For ordinary instructions it increases by the instruction width at every step; this is why a program flows “top to bottom”.

This cycle is the definition of everything the processor does. The difference between an operating system, a browser, or a game lies not in the cycle but in the sequence of instructions it reads.

Overlapping the Steps

The steps of the cycle use different hardware units: fetching occupies the memory interface, decoding the control unit, executing the arithmetic unit. Waiting for each step in sequence leaves most of these units idle at any given moment.

A pipeline closes this gap: while one instruction executes, the next is decoded, and a third is fetched. The time to complete a single instruction does not change; the number of instructions completed per unit of time increases.

The cost of a pipeline is branching. In a conditional branch, the address of the next instruction is unknown until the condition is computed; which instructions to feed into the pipeline is uncertain. The processor makes a prediction, and if the prediction is wrong, it discards the instructions already in the pipeline. Branches that are hard to predict therefore carry a measurable cost.

This repeats an observation from the first topic: instruction count is not the sole determinant of duration.

Changing the Flow

The fact that the program counter can be written to directly is the source of every control structure.

An unconditional branch writes a new address to the program counter; flow continues from there. A conditional branch first examines the result of a comparison. Comparison instructions write their result to flag bits — whether the result is zero, negative, or overflowed. A conditional branch reads these flags and changes the program counter only if the condition holds.

The correspondence with high-level constructs is built this way:

Construct Instruction equivalent
if Compare; branch to the end of the block if the condition fails
while A branch testing the condition + a branch back to the top at the end of the body
Function call Save the return address, branch to the target; on return, branch to the saved address

Where the return address is saved during a function call is the function of the call stack; stack layout is covered in this topic’s final lesson.

The following program simulates the fetch-decode-execute cycle on the teaching encoding:

OP_NAMES = {0x41: "add", 0x42: "subtract", 0xFF: "halt"}


def decode(instruction: int) -> tuple[str, int, int, int]:
    """Splits a 32-bit instruction into its fields."""
    op   = (instruction >> 24) & 0xFF
    dest = (instruction >> 16) & 0xFF
    src1 = (instruction >> 8) & 0xFF
    src2 = instruction & 0xFF
    return OP_NAMES.get(op, "unknown"), dest, src1, src2


def run(memory: list[int], registers: dict[int, int]) -> None:
    pc = 0                                   # program counter
    while pc < len(memory):
        op, dest, s1, s2 = decode(memory[pc])
        if op == "halt":
            break
        if op == "add":
            registers[dest] = registers.get(s1, 0) + registers.get(s2, 0)
        elif op == "subtract":
            registers[dest] = registers.get(s1, 0) - registers.get(s2, 0)
        pc += 1                              # advance to the next instruction


print(decode(0x41424344))    # ('add', 66, 67, 68)

memory = [0x41424344, 0x42024344, 0xFF000000]
registers = {67: 10, 68: 4}
run(memory, registers)
print(registers[66], registers[2])  # 14 6

The pc += 1 line is the program counter’s ordinary advance. If a branch instruction were added, all it would do is write a different value to this variable.

Summary

  • Instructions reside in memory just like data; the distinction lies in the interpretation applied to the bytes.
  • The instruction set is the contract between hardware and software, and it contains a small number of instruction classes.
  • An instruction is a bit pattern divided into opcode and operand fields; field layout is architecture-specific.
  • The processor continuously repeats the fetch-decode-execute cycle; the difference between programs lies not in the cycle but in the sequence of instructions read.
  • The program counter holds the address of the next instruction; conditional and unconditional branches produce every control structure by changing this register.

Next Step

Every turn of the cycle requires at least one memory access, and as the previous lesson showed, main memory is hundreds of times slower than the processor. The next lesson covers the cache layer that closes this gap, and the access pattern a program must follow for the cache to work efficiently.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close