Computing and the Command Line

Instructions and the Fetch-Execute Cycle

Almost every computer follows the von Neumann design: a CPU, made of a control unit and a processing unit, and a memory that holds the program's instructions and its data side by side. Instructions are bits in a format the processor's instruction set defines; machine code and assembly, from a real disassembly; the four stages of executing one instruction, fetch, decode, execute, write back, driven by the program counter; jumps for loops and if; and Python's bytecode as a similar instruction list for a virtual machine.

  • 6 min
  • 7 steps
  • 2 questions
  • Lesson 69 of 80

In this lesson

  1. A stored program
  2. Instructions are bits
  3. The cycle
  4. Jumps: loops and if
  5. Python’s version
  6. Your turn
  7. So

A stored program

Nearly every computer today, from a phone to a server, follows the von Neumann architecture. It has five parts 1:

  • a processing unit that does the work: the ALU (arithmetic logic unit, last module) and a set of registers, small fast storage for the values being worked on;
  • a control unit that runs the show, holding the program counter (PC), the address of the next instruction, and the instruction register (IR), the instruction being carried out;
  • memory;
  • input and output devices;
  • buses, sets of wires carrying addresses, data, and control signals between them.

The control and processing units together are the CPU 1. The key idea is in the memory: it holds the program’s instructions and its data side by side. Instructions are just data, bits in memory 1. That’s what makes a computer general-purpose: running a different program means putting different bytes in memory, not rewiring anything.

Left, the von Neumann architecture: a CPU containing a control unit, with the PC holding the next address and the IR holding the current instruction, and a processing unit with the ALU for arithmetic and logic and registers Reg0, Reg1, and so on; buses for addresses, data, and control connect the CPU to memory, which holds instructions and data side by side, a stored program, for example address 1234 holding ADD R0,R1,R3 and address 5000 holding 42, and to input and output: keyboard, screen, disks, network. Right, one instruction, ADD Reg0, Reg1, Reg3: 1, fetch, read the instruction at the PC into the IR, and the PC moves on; 2, decode, split the bits, opcode ADD to the ALU, select Reg1 and Reg3; 3, execute, the ALU adds the two values; 4, write back, store the result in Reg0; and again, billions of times a second.
Instructions and data share one memory; the CPU fetches, decodes, executes, and writes back. Credit: StudyCorner diagram · CC BY 4.0 · Source

Quick check

What’s special about the ‘stored program’ in the von Neumann design?

Instructions are bits

Each kind of processor understands a fixed set of instructions, its instruction set architecture or ISA: “add these two registers,” “load this value from memory,” “jump to this address.” Intel and AMD processors use the x86-64 instruction set; phones, Raspberry Pis, and Apple’s recent Macs use ARM. A program compiled for one won’t run on the other, which is why Ubuntu’s download page offers separate images for each (Linux course, module 1).

me@linuxbox:~$ uname -m
x86_64

An instruction is a string of bits in a format the ISA defines: some bits say which operation (the opcode, such as ADD), others say which registers or memory locations to use 1. That’s machine code. People read it as assembly, a text form with one line per instruction. Here’s a tiny C function, return a + 2;, compiled for x86-64 and disassembled with objdump -d 1:

0000000000400526 <adder2>:
  400526:       55                      push   %rbp
  400527:       48 89 e5                mov    %rsp,%rbp
  40052a:       89 7d fc                mov    %edi,-0x4(%rbp)
  40052d:       8b 45 fc                mov    -0x4(%rbp),%eax
  400530:       83 c0 02                add    $0x2,%eax
  400533:       5d                      pop    %rbp
  400534:       c3                      retq

Each line is an address in memory, the instruction’s bytes in hex, and the assembly it stands for. 55 is the whole of push %rbp; 83 c0 02 is “add 2 to register %eax.” One line of C became seven instructions, and the actual addition is just one of them 1.

The cycle

The CPU runs a program by repeating four stages for each instruction 1:

  1. Fetch. Read the instruction at the address in the PC from memory into the IR, and move the PC on to the next instruction. (With 4-byte instructions, PC goes up by 4.)
  2. Decode. Split the instruction’s bits: the opcode goes to the ALU to choose the operation, and the register numbers select which registers to read.
  3. Execute. The ALU does the operation, say adding Reg1 and Reg3.
  4. Write back. Store the result, in Reg0 for example.

Then fetch the next one. That’s all a CPU does, billions of times a second.

Quick check

What does the program counter hold?

Jumps: loops and if

Programs don’t just run straight down. A jump (or branch) instruction changes the PC to a different address, so the next fetch comes from somewhere else 1. A conditional jump only does it if a condition holds, like a comparison’s result. Every if, every loop, every function call is built from jumps.

Python’s version

Python doesn’t compile to machine code. It compiles to bytecode, instructions for a virtual machine, the Python interpreter, itself a program running on the real CPU. But it’s the same idea, and you can look at it with the dis module 2:

me@linuxbox:~$ python3 -c 'import dis; dis.dis("total = price + tax")'
  0           0 RESUME                   0

  1           2 LOAD_NAME                0 (price)
              4 LOAD_NAME                1 (tax)
              6 BINARY_OP                0 (+)
             10 STORE_NAME               2 (total)
             12 RETURN_CONST             0 (None)

Load the two values, add them, store the result: fetch, decode, execute, write back, in miniature. And an if becomes a conditional jump:

me@linuxbox:~$ python3 -c 'import dis; dis.dis("if n > 0:\n    n = n - 1")'
  ...
  1           2 LOAD_NAME                0 (n)
              4 LOAD_CONST               0 (0)
              6 COMPARE_OP              68 (>)
             10 POP_JUMP_IF_FALSE        6 (to 24)

  2          12 LOAD_NAME                0 (n)
             14 LOAD_CONST               1 (1)
             16 BINARY_OP               10 (-)
             20 STORE_NAME               0 (n)
  ...

POP_JUMP_IF_FALSE skips ahead to offset 24 when n > 0 is false. (This is from Python 3.12; bytecode changes a little between versions 2.)

Your turn

Exercises

  1. uname -m on your Linux machine. Which instruction set does it run?
  2. In the objdump listing, how many bytes long is each instruction? Do x86-64 instructions all have the same length?
  3. Run python3 -c 'import dis; dis.dis("a = b * c - d")'. Which instruction does each step?
  4. Try a loop: python3 -c 'import dis; dis.dis("while n:\n n = n - 1")'. Find the jump that goes backward.
  5. In your own words: what happens to the PC during a jump?
Answers
  1. x86_64 on Intel and AMD; aarch64 on 64-bit ARM.
  2. 1, 3, 3, 3, 3, 1, and 1 bytes: x86-64 instructions vary in length. Some other instruction sets use a single fixed length.
  3. Two LOAD_NAMEs, a BINARY_OP for *, another LOAD_NAME, a BINARY_OP for -, and STORE_NAME.
  4. JUMP_BACKWARD 8 (to 6) sends execution back to offset 6, the start of the loop body, after the test at offset 18 finds n still non-zero.
  5. Instead of moving on to the next instruction, it’s set to the jump’s target address, so the next fetch comes from there.

So

A von Neumann computer has a CPU (control unit and processing unit) and a memory that holds instructions and data alike: a stored program. Instructions are bits in a format the instruction set (x86-64, ARM) defines; machine code is those bits, assembly their text form. The CPU loops through fetch (instruction at the PC into the IR, PC moves on), decode, execute (in the ALU), and write back. Jumps change the PC, making loops and ifs. Python compiles to bytecode for its own virtual machine, which works the same way and which dis shows.

Lesson complete

Nice work.

1day streak
0/1today's goal
–correct

Up next · 5 min

The Clock, Pipelines, and Cores

Next lesson
Sources for this lesson
  1. 1
    Suzanne J. Matthews, Tia Newhall, Kevin C. Webb. Dive into Systems. No Starch Press (free online edition). 2022. verifiedCh. 4 Binary and Data Representation: bits as two voltage states, bytes (8 bits, 256 values, smallest addressable unit), words of 32 or 64 bits, n bits give 2^n values; decimal and binary place value with 0b and 0x prefixes; hexadecimal as four bits per digit; fixed storage sizes and unsigned ranges; two's complement with a negative-weighted top bit, one zero, range -2^(n-1) to 2^(n-1)-1, all ones is -1, negation by flipping bits and adding one; subtraction as adding the negation, reusing negation and addition circuits; overflow and the odometer analogy.
  2. 2
    dis: Disassembler for Python bytecode (Python documentation). Python Software Foundation. verifiedThe dis module disassembles CPython bytecode, the instructions the interpreter executes; bytecode is a CPython implementation detail and may be added, removed, or changed between Python versions.