Computer & OS Fundamentals · beginner · ~10 min

How a computer runs a program

**What you will learn** - Trace the full path a program takes from source code to a running process the CPU executes. - Describe the CPU's fetch-decode-execute cycle and the role of the *program counter*. - Explain what *machine code* is and how compiled and interpreted code both end up as machine instructions. - Define the *von Neumann architecture* and explain why storing code and data in the same memory has consequences for safety and security. - Read a simple text diagram of CPU + memory and predict which instruction runs next. - Connect this mental model to everyday tasks: debugging, performance, and understanding why memory bugs can change program behavior.

Overview

Every app you use — a browser, a game, a chat program — is ultimately a list of tiny instructions that a chip called the CPU (central processing unit) carries out one at a time. Before any of that can happen, the program you wrote as readable text has to be turned into a form the hardware understands, loaded into memory, and handed to the CPU.

This lesson follows that journey end to end. It is the foundation for everything else in this track: once you understand that a running program is just data being read and acted on by a simple, fast loop, ideas like compiling, memory, processes, and even security bugs stop feeling like magic.

The CPU itself does something surprisingly simple. It repeats one loop billions of times per second: read the next instruction, figure out what it means, do it, repeat. That loop is called fetch-decode-execute. A small storage slot inside the CPU, the program counter, remembers the address of the next instruction so the CPU always knows where to look.

The instructions the CPU reads are machine code — raw numbers that encode operations like "add these two values" or "jump to this address." You almost never write machine code by hand. Instead, you write in a higher-level language. A compiled language like C is translated ahead of time into machine code by a compiler. An interpreted language is read and run by another program, an interpreter, while it executes. Either way, what the hardware actually runs is always machine code.

Finally, this lesson introduces the von Neumann architecture: the design, used by nearly every computer you will ever touch, where instructions and data live together in the same memory. That single design choice is why programs are flexible — and why a bug that corrupts data can sometimes corrupt instructions too.

Why it matters

This mental model pays off constantly, even far from security work.

  • Debugging. When a program crashes, the message often points at an address or an instruction. Knowing that the CPU walks through instructions in order, guided by the program counter, helps you read a crash report instead of fearing it.
  • Performance. Why is one program fast and another slow? Both run on the same fetch-decode-execute loop. Understanding that loop is the first step toward understanding why memory access patterns and instruction counts matter.
  • Understanding tools. Compilers, interpreters, debuggers, and profilers all manipulate the same pipeline: source -> machine code -> memory -> CPU. Knowing the pipeline lets you reason about what each tool is doing.
  • Safety and security. Because code and data share one memory (von Neumann), a bug that writes past the end of a data buffer can, in the worst case, overwrite values the CPU later treats as instructions or as the address of the next instruction. This is the seed idea behind many classes of memory-safety bugs. You do not need to attack anything to benefit from this: it explains why careful, bounds-checked code matters, a theme you will see throughout the C track.

Core concepts

1. The CPU and the fetch-decode-execute cycle

Definition. The CPU is the chip that executes instructions. Its core behavior is a repeating loop called the fetch-decode-execute cycle.

Plain language. The CPU is like a very fast, very literal worker who can only follow one instruction at a time off a list. It reads the next instruction, understands it, does it, and immediately reaches for the next one.

How it works internally. Each pass through the loop has three steps:

  1. Fetch — copy the next instruction from memory into the CPU. The CPU knows which instruction is next because the program counter holds its address.
  2. Decode — interpret the bits of the instruction to figure out the operation and its operands (for example: "add the number in slot A to the number in slot B").
  3. Execute — actually perform the operation: do arithmetic, read or write memory, or change the program counter to jump elsewhere.

After execute, the program counter normally advances to the following instruction, and the loop repeats — billions of times per second.

        +-------------------- CPU --------------------+
        |                                             |
        |   [ Program Counter ] --> address of next   |
        |          |                                  |
        |          v                                  |
        |   1. FETCH instruction from memory          |
        |          |                                  |
        |          v                                  |
        |   2. DECODE what it means                   |
        |          |                                  |
        |          v                                  |
        |   3. EXECUTE it  --> may change PC (jump)    |
        |          |                                  |
        |          +--------> loop back to FETCH       |
        +---------------------------------------------+

When this model matters. Any time you reason about order of execution, jumps, loops, or why a crash happened at a specific point. When not to over-apply it: for everyday coding you usually think in higher-level terms (loops, functions); you do not trace single CPU instructions unless you are debugging at a very low level.

Common pitfall. Beginners imagine the CPU "sees the whole program at once." It does not. It only ever knows the one address in the program counter and works strictly step by step.

Knowledge check (explain in your own words): In your own words, what are the three steps of the cycle, and which one can change where the program goes next?

2. The program counter

Definition. The program counter (PC) is a small, fast register inside the CPU that holds the memory address of the next instruction to fetch.

Plain language. It is the CPU's bookmark. It does not hold the instruction itself — it holds where the instruction is.

How it works. After each instruction, the PC usually moves forward to the next instruction. But some instructions (jumps, calls, returns, branches from if/loops) deliberately set the PC to a different address. That is how programs make decisions and repeat.

Memory addresses:   100   104   108   112   116
Instructions:      [ADD] [SUB] [JMP] [...] [...]
                                  |
PC = 100 -> fetch ADD, PC -> 104
PC = 104 -> fetch SUB, PC -> 108
PC = 108 -> fetch JMP 100  (sets PC back to 100!)
PC = 100 -> fetch ADD again ... (a loop)

When to think about it. Whenever control flow is involved: loops, conditionals, function calls. Pitfall: confusing the PC's contents (an address) with the instruction stored at that address. The PC holds the address, not the operation.

Knowledge check (predict): In the diagram above, the JMP at address 108 sends the PC back to 100. What kind of program structure does this create?

3. Machine code, compiling, and interpreting

Definition. Machine code is the set of raw numeric instructions a specific CPU can execute directly. It is the only language the hardware truly understands.

Plain language. You write friendly text like x = a + b;. The CPU cannot read that. Something must translate it into numbers that mean "add."

Two ways to get there:

Compiled (e.g., C) Interpreted (e.g., Python)
When translation happens Ahead of time, by a compiler While running, by an interpreter
What ships to the user Machine code (an executable) The source, plus an interpreter
Typical speed Faster (already machine code) Slower (translated as it runs)
What the CPU runs The machine code directly The interpreter's machine code, which reads your source

The key insight: either way, the CPU only ever executes machine code. With a compiled language, your code becomes machine code. With an interpreted language, the interpreter is the machine-code program, and it reads your source as data.

Compiled path:
  source.c --(compiler)--> machine code --> loaded into memory --> CPU runs it

Interpreted path:
  source.py --read by--> interpreter (itself machine code) --> CPU runs interpreter

When to use which is usually decided by the language, not by you per-program. Pitfall: thinking interpreted languages "don't use machine code." They do — the interpreter is machine code; your script is its input.

Knowledge check (find the misconception): A friend says, "Python doesn't run on the CPU, it runs in the interpreter, so machine code isn't involved." What is wrong with this statement?

4. Von Neumann architecture: code and data share memory

Definition. The von Neumann architecture is a computer design in which both instructions (code) and the values they work on (data) are stored in the same memory.

Plain language. There is one big pool of numbered storage slots. Some slots happen to hold instructions; others hold data. The hardware does not paint them different colors — a slot is just a slot.

How it works / why it is powerful. Because code is stored like any other data, a program can be loaded, copied, and replaced easily. That flexibility is why you can install new software without rewiring anything. The CPU simply points the program counter at wherever the new code lives.

          Single shared memory
  +------------------------------------------+
  | addr 100: instruction (code)             |
  | addr 104: instruction (code)             |
  | addr 108: instruction (code)             |
  | ...                                      |
  | addr 500: data (e.g., your input)        |
  | addr 504: data                           |
  +------------------------------------------+
         ^                      ^
      code lives here       data lives here
        (same memory, just different addresses)

The trade-off (why it matters for robustness and safety). Since code and data live together, a bug that writes data into the wrong place can, in extreme cases, corrupt memory the CPU later treats as instructions or as the address of the next instruction. This is exactly why writing carefully within the bounds of your buffers matters in C — a theme this track returns to often.

Pitfall: assuming the computer "knows" which bytes are code and which are data. In a pure von Neumann machine it does not inherently; correctness depends on the program (and the operating system's protections) keeping them straight.

Knowledge check (concept): Name one benefit and one risk of storing code and data in the same memory.

Syntax notes

This is a concept lesson, so there is no programming syntax to memorize. Instead, fix this vocabulary, because every later lesson reuses it:

  • CPU — the chip that executes instructions.
  • Register — a tiny, very fast storage slot inside the CPU (the program counter is one register).
  • Program counter (PC) — the register holding the address of the next instruction.
  • Address — a number that names a location in memory.
  • Machine code — numeric instructions the CPU runs directly.
  • Compiler — a tool that translates source code into machine code ahead of time.
  • Interpreter — a program that reads source code and runs it while executing.
  • Von Neumann architecture — a design where code and data share one memory.

A handy one-line summary of the whole pipeline:

source code --> (compiler or interpreter) --> machine code in memory --> CPU fetch-decode-execute

Lesson

A program is just data until the CPU executes it. Understanding that path is the foundation for everything from debugging to exploitation.

The fetch-decode-execute cycle

The CPU repeats one loop billions of times per second:

  1. Fetch the next instruction from memory. Its address is held in the program counter (a small, fast storage slot inside the CPU).
  2. Decode what the instruction means.
  3. Execute it. This might be arithmetic, a memory read or write, or a jump to another address.

From source to execution

For a compiled language like C, the path is:

source -> compiler -> machine code -> loaded into memory -> executed

For an interpreted language, an interpreter reads and runs the source directly. The interpreter is itself a compiled program.

Either way, what the CPU runs is always machine code.

Why a pentester cares

Memory-corruption bugs, shellcode, and reverse engineering all live at this layer.

Two facts matter most:

  • Code and data share the same memory.
  • The CPU blindly executes whatever the program counter points at.

Put those together and you see why overwriting a return address can hijack execution. That is the core idea behind classic exploitation.

Code examples

Because this is a concept lesson, the "code" here is a plain-text trace of the fetch-decode-execute cycle for a tiny made-up program. It is pseudo-assembly (not a real CPU's instruction set) chosen so you can read it without prior assembly knowledge. The goal is to see the loop run.

Program (stored in memory, one instruction per address):

  addr 100:  LOAD  R1, [500]    ; copy the data at address 500 into register R1
  addr 104:  LOAD  R2, [504]    ; copy the data at address 504 into register R2
  addr 108:  ADD   R3, R1, R2   ; R3 = R1 + R2
  addr 112:  STORE [508], R3    ; copy R3 into memory at address 508
  addr 116:  HALT               ; stop the CPU

Data (also in memory, same address space):

  addr 500:  7
  addr 504:  5
  addr 508:  (empty, will receive the result)

Start: program counter (PC) = 100

What it does. This program adds two numbers that live in memory (7 and 5), then stores the result (12) back into memory. Notice that the instructions (addresses 100-116) and the data (addresses 500-508) sit in the same numbered memory — that is the von Neumann idea in action.

Expected result. After the program runs, memory address 508 holds 12, and the CPU stops at the HALT instruction.

Key edge cases to keep in mind:

  • If the program counter were ever set to an address that holds data instead of an instruction (say, PC = 500), the CPU would try to decode the number 7 as an instruction. On real hardware this typically causes a crash or undefined behavior — a direct consequence of code and data sharing memory.
  • If address 508 already held an important value, the STORE would overwrite it. The CPU does not warn you; it does exactly what it is told.

Line by line

Let's walk the cycle step by step, watching the program counter (PC) and registers change. Each row is one full fetch-decode-execute pass.

Step PC before Instruction fetched What execute does PC after
1 100 LOAD R1, [500] R1 = value at addr 500 = 7 104
2 104 LOAD R2, [504] R2 = value at addr 504 = 5 108
3 108 ADD R3, R1, R2 R3 = 7 + 5 = 12 112
4 112 STORE [508], R3 memory[508] = R3 = 12 116
5 116 HALT CPU stops

Narration of what happens:

  1. Start: PC = 100. The CPU fetches the instruction at 100, decodes it as a LOAD, and executes it by reading memory address 500 (the number 7) into register R1. With no jump involved, the PC advances to 104.
  2. The CPU fetches the LOAD at 104 and pulls the number 5 from address 504 into R2. PC advances to 108.
  3. The CPU fetches the ADD at 108. Both operands are now sitting in registers (7 and 5), so execution computes 12 and places it in R3. No memory access is needed for the addition itself. PC advances to 112.
  4. The CPU fetches the STORE at 112 and writes R3 (12) out to memory address 508. PC advances to 116.
  5. The CPU fetches HALT at 116 and stops.

Memory at the end: addresses 500 and 504 are unchanged (7 and 5); address 508 now holds 12. The result is produced because each instruction did exactly one small, predictable step, and the PC marched through them in order — the entire essence of the fetch-decode-execute loop.

Common mistakes

Mistake 1: Thinking the program counter holds the instruction.

  • Wrong idea: "The PC contains ADD R3, R1, R2."
  • Why it is wrong: The PC holds an address (like 108). The instruction is stored at that address in memory. The CPU uses the PC to know where to fetch from.
  • Corrected understanding: PC = 108; memory[108] = the ADD instruction. The PC is a pointer-like bookmark, not the content.
  • How to catch it: Whenever you say "the PC," ask yourself "is this a number naming a location, or the thing at that location?" It is always the location.

Mistake 2: Believing interpreted languages avoid machine code.

  • Wrong idea: "Compiled languages use machine code; interpreted ones don't."
  • Why it is wrong: The CPU can only run machine code, period. An interpreter is itself a compiled, machine-code program; your script is the data it reads.
  • Corrected understanding: Source -> read by interpreter (machine code) -> CPU runs interpreter. Machine code is always involved.
  • How to catch it: Ask "what is the CPU actually executing right now?" The answer is always machine code.

Mistake 3: Assuming the computer knows which bytes are code and which are data.

  • Wrong idea: "Memory keeps code and data in separate, labeled areas the hardware understands."
  • Why it is wrong: In the von Neumann model there is one shared memory; a byte is a byte. Correct behavior depends on the program (and OS protections) pointing the PC only at real instructions.
  • Corrected understanding: If the PC ends up pointing at data, the CPU will blindly try to decode that data as an instruction.
  • How to catch it: When reasoning about a crash "in the middle of nowhere," consider whether execution jumped to an address that does not hold valid code.

Mistake 4: Imagining the CPU runs many instructions "at once" because computers are fast.

  • Wrong idea: "Fast = parallel = all instructions together."
  • Why it is wrong: The basic mental model is one instruction at a time in order. (Real CPUs do clever overlapping, but for learning, treat it as sequential.)
  • Corrected understanding: Billions of sequential loop passes per second feel instantaneous but are still ordered.
  • How to catch it: When predicting output, trace instructions one at a time in PC order.

Debugging tips

Even for a concept this foundational, here are the practical signs that your mental model (or, later, your real code) is off, and how to recover.

Logic-error symptoms in your reasoning:

  • You predicted the wrong final value. Re-trace using a table like the one in the walkthrough: write the PC and every register on each line. Most mistakes come from skipping a step or forgetting that an instruction advanced the PC.
  • You lost track of control flow. Whenever an instruction is a jump/branch (or, in real code, an if, loop, or function call), explicitly write the new PC value before continuing.

When real programs misbehave (looking ahead to the C track):

  • A crash report mentions an address or "segmentation fault." This often means the program tried to read/write memory it should not, or the PC was sent somewhere invalid. Tie it back to: the CPU did exactly what the PC and instructions told it to.
  • Compiler errors happen before anything runs — the translator could not produce machine code at all. Fix the source first; there is no PC or execution to debug yet.
  • The program runs but gives wrong answers (logic errors) mean the machine code is valid but expresses the wrong steps. Trace the intended sequence by hand.

Questions to ask when it does not work:

  1. What instruction is the CPU on (what is the PC)?
  2. What does that instruction read or change?
  3. Did a jump/branch send the PC where I expected?
  4. Am I confusing an address with the value stored there?
  5. (Compiled code) Did it even compile, or am I debugging code that never became machine code?

Memory safety

This is a non-security concept lesson, so the focus here is on robustness and building correct intuition rather than attacks.

The single most important takeaway is a consequence of the von Neumann architecture: code and data share one memory. Two robustness implications follow.

  • Writing outside intended bounds is dangerous. If a program writes past the end of where it is supposed to, it can clobber other data — and in the worst case, values the CPU later relies on (such as the address it will jump to next). You do not need to understand exploitation to take the lesson: stay within the bounds of your data. In the C track you will see exactly how arrays and buffers make this concrete.
  • Uninitialized or stray addresses cause undefined behavior. If execution or a memory access lands on an address that does not hold what you assumed (data treated as code, or an unset pointer), the result is unpredictable: a crash, wrong output, or worse. The defensive habit is to initialize values, validate inputs, and never assume the contents of memory you did not set.

Modern operating systems add protections (for example, marking the regions that hold code as read-only and refusing to execute regions meant only for data). These protections exist precisely because the underlying von Neumann design would otherwise let code and data be confused. Knowing why those protections exist makes the rest of this track easier to understand.

Real-world uses

Concrete real-world uses of this model:

  • Every device you own. Phones, laptops, smart TVs, game consoles, and microcontrollers in appliances and cars all run a fetch-decode-execute loop over machine code in shared memory. The same mental model scales from a $2 microcontroller to a server farm.
  • Operating systems and loaders. When you double-click an app, the OS loads its machine code into memory and sets the program counter to the program's entry point. Understanding this is the first step toward understanding processes (your next lesson, "CPU, RAM, disk, and processes").
  • Debuggers and profilers. Tools like gdb let you watch the program counter, single-step instructions, and inspect registers — they are literally exposing the loop you learned here. Profilers sample which instruction the PC is on to find slow spots.
  • Compilers and interpreters. The translation step you learned about is the entire job of toolchains like GCC (compiler) or CPython (interpreter).

Professional best-practice habits this lesson seeds:

Beginner habits:

  • Think in terms of order of execution — programs do one thing, then the next.
  • Keep clear in your mind the difference between an address and the value at that address. This pays off enormously when you reach pointers in C.
  • Read error messages literally: the machine did exactly what it was told.

More advanced habits:

  • When reasoning about performance, remember every line becomes some number of machine instructions and memory accesses; fewer and more cache-friendly is usually faster.
  • Treat the boundary between code and data with respect — validate inputs and stay within buffer bounds, because the hardware will not protect you by default.
  • Use the right tool for the right layer: a debugger to inspect execution, a profiler to inspect performance, the compiler's warnings to catch problems before anything runs.

Practice tasks

Beginner 1 — Trace the cycle.

  • Objective: Show you can run the fetch-decode-execute loop by hand.
  • Requirements: Using the example program from the "code" section, but with the data values changed to address 500 = 9 and address 504 = 4, produce a trace table with columns: step, PC before, instruction, what executes, PC after.
  • Expected output: The final value stored at address 508, and the final PC.
  • Hints: Copy the table format from the walkthrough. Remember that no-jump instructions advance the PC by 4 each time.
  • Concepts: fetch-decode-execute, program counter, registers.

Beginner 2 — Address vs. value.

  • Objective: Cement the difference between an address and the value at it.
  • Requirements: In one or two sentences each, explain (a) what the program counter holds, and (b) why "PC = 108" is different from "the ADD instruction." Then give a real-life analogy (e.g., a house number vs. the people inside).
  • Constraints: No code; plain language.
  • Hints: Reuse the bookmark analogy from the lesson but make it your own.
  • Concepts: program counter, address, machine code.

Intermediate 1 — Compiled vs. interpreted.

  • Objective: Explain both paths precisely.
  • Requirements: Draw (in text) two pipelines — one compiled, one interpreted — from source code to the CPU. For each, label exactly what the CPU is executing. Then write two sentences correcting the myth "interpreted languages don't use machine code."
  • Hints: The interpreter is itself a machine-code program; your script is its input.
  • Concepts: compiler, interpreter, machine code.

Intermediate 2 — Add a loop.

  • Objective: Show how the program counter creates repetition.
  • Requirements: Extend the example pseudo-program so it adds the value at address 500 to a running total three times using a jump (JMP) and a counter. Describe what the PC does on each pass and how the loop eventually ends.
  • Input/output example: If address 500 = 6, the total after three passes should be 18.
  • Constraints: You may invent simple pseudo-instructions like DEC, JNZ (jump if not zero); state what each means.
  • Hints: A loop is just an instruction that sets the PC backward; something must change so it eventually stops.
  • Concepts: program counter, jumps, control flow.

Challenge — Predict the crash.

  • Objective: Reason about the von Neumann consequence.
  • Requirements: Suppose a buggy instruction sets the program counter to address 500 (which holds the data value 7) instead of a real instruction. Explain, step by step, what the CPU tries to do next, and why this leads to undefined behavior or a crash. Then describe one defensive habit and one OS protection that reduce this risk.
  • Constraints: Conceptual explanation only — no real exploit, no real system. Keep it to the model in this lesson.
  • Hints: The CPU does not know 7 is "data"; it will try to decode it. Connect this to staying within buffer bounds.
  • Concepts: von Neumann architecture, program counter, robustness/safety.

Summary

  • The CPU runs every program through one simple, fast loop: fetch the next instruction, decode it, execute it, repeat — billions of times per second.
  • The program counter is the CPU's bookmark: it holds the address of the next instruction, not the instruction itself. Jumps, loops, and function calls work by changing it.
  • The CPU only ever runs machine code. A compiler turns source into machine code ahead of time; an interpreter is a machine-code program that reads your source as it runs. Either way, machine code is what executes.
  • The von Neumann architecture stores code and data in the same memory. This makes software flexible, but it also means careless writes can corrupt memory the CPU depends on — the reason staying within bounds and validating input matters.
  • Common mistakes to avoid: confusing an address with the value at it; thinking interpreted languages skip machine code; assuming the hardware inherently separates code from data; and imagining instructions run all at once instead of in order.
  • Remember: a running program is just data being walked through by a simple loop. Hold onto that, and processes, memory, debugging, and safety will all make sense as you continue to "CPU, RAM, disk, and processes."

Practice with these exercises