Foundations: from source to running program · about 30 min

What a program is

Why this matters

Every language you will ever use, from Python to JavaScript to Rust, eventually runs as machine instructions on a processor that knows nothing about variables, functions or objects. C sits closer to that machine than almost any other language you are likely to write. Learning C is therefore also learning what a computer really does with your code. That knowledge pays off everywhere: it explains why some programs are fast and others slow, why some bugs crash a program and others silently corrupt data, and how the operating system you use every day is built. Linux, the Python interpreter, Git, SQLite, and the firmware in your router are all written in C.

This first lesson has no new syntax. It builds the mental model the rest of the track stands on.

The machine: processor, memory, storage

A computer, reduced to the parts that matter for programming, has three pieces:

 +-------------------+        +-----------------------------------+
 |       CPU         | <----> |            RAM (memory)           |
 |  registers        |        |  byte 0, byte 1, byte 2, ...      |
 |  arithmetic unit  |        |  billions of numbered bytes       |
 |  program counter  |        +-----------------------------------+
 +-------------------+                         ^
                                               | loaded from
                                    +----------+---------+
                                    |  disk (files)      |
                                    |  programs, data    |
                                    +--------------------+

The CPU does its work in a loop that never stops while the machine is on: fetch the instruction at the address in the program counter, decode what it means, execute it, move the program counter forward (or somewhere else, for a jump), repeat. Billions of times per second. Everything a computer does, from playing video to running a web server, is this loop executing simple instructions.

Machine code

Instructions are stored in memory as bytes, just like data. On an x86-64 processor (most desktop and server PCs), the bytes 48 01 d8 mean "add the value in register rbx to register rax". Different processor families (x86-64, ARM64 as in phones and Apple Silicon Macs, RISC-V) have different instruction sets, so the same bytes mean different things, or nothing, on another family. Machine code is the only language a CPU understands.

Writing raw bytes is miserable, so the first step up is assembly language: a readable spelling of machine code, one line per instruction.

add  rax, rbx      ; machine code bytes: 48 01 d8

Assembly is still tied to one instruction set and makes you manage every register by hand. A program of any size written this way is long, slow to write and impossible to move to another kind of computer.

Source code and the compiler

C was created in the early 1970s by Dennis Ritchie at Bell Labs to write the Unix operating system in something more humane than assembly, while staying almost as efficient. You write source code: text in a file, usually ending in .c. A program called a compiler translates that text into machine code for your processor and saves the result as an executable file. Then you run the executable.

  hello.c  (text you write)
     |
     |  compiler (gcc)
     v
  hello    (machine code, an executable file)
     |
     |  you run it: the operating system loads it into memory,
     |  points the CPU at its first instruction
     v
  output on your terminal

The key consequences of this model:

Note: Python, which you may know, is usually interpreted: a program called python3 (itself written in C) reads your script while it runs and carries out each step. That makes starting easier and running slower. C has no interpreter between your code and the CPU.

What "running a program" means

When you type ./hello in a terminal, a lot happens that you never see:

  1. The shell asks the operating system kernel to start a new process (a running instance of a program) and load the file hello into it.
  2. The kernel reads the executable, sets up memory for it: a region for its machine code, a region for its global data, a stack for function calls, and room for a heap that can grow as the program asks for memory.
  3. The kernel points the CPU at the program's entry point. A little startup code from the C library runs first, then calls your main function.
  4. Your code runs. When it wants to print, it cannot touch the screen directly; it asks the kernel to write bytes to the terminal through a system call.
  5. When main returns, the process exits and hands a small number, the exit status, back to the shell. The kernel reclaims all of the process's memory.
  process memory (simplified, low addresses at the bottom)

  +--------------------+  high addresses
  |       stack        |  function calls, local variables (grows down)
  |         |          |
  |         v          |
  |                    |
  |         ^          |
  |         |          |
  |        heap        |  memory requested while running (grows up)
  +--------------------+
  |   global data      |
  +--------------------+
  |   machine code     |  your compiled functions
  +--------------------+  low addresses

You do not need to memorize this now. Every box in that picture gets its own lesson later, and by the end of the track you will be able to point at any variable in a C program and say which region it lives in and when it disappears.

Why C is the way it is

C gives you very direct control: you choose how many bytes each number takes, you can ask for the address of any variable, you request and release memory yourself. In exchange, C trusts you completely. It does not check array bounds, does not clean up memory for you, and treats many mistakes as undefined behavior: the language standard places no requirement on what happens, so the program may crash, may print garbage, or may appear to work today and fail next month. That trust is why C is fast and why it can be used to write operating systems, and it is also why C programmers develop careful habits and lean on tools such as compiler warnings, gdb and valgrind, all of which this track teaches.

Common mistakes in thinking

How exercises work in this track

Each lesson ends with exercises. In code exercises you edit main.c; it is compiled with

$ gcc -std=c17 -Wall -Wextra -g -O0 -o prog main.c -lm

and run as ./prog inside a sandbox. Warnings are shown but do not fail the check (read them anyway: they are usually right). Each part of that command is explained in the next lesson. Your output must match the expected output exactly, apart from trailing spaces and trailing blank lines.

Recap

Going further: run file /bin/ls in the sandbox terminal: it reports that ls is an "ELF 64-bit LSB pie executable, x86-64". ELF is the Linux executable format, and x86-64 is the instruction set it was compiled for.

Exercises

Exercise 1 · quiz

What the CPU actually runs

When you run a compiled C program, what does the processor execute?

Answer
  • Machine code: numeric instructions for that specific kind of processor

The CPU only understands machine code: instructions encoded as bytes, specific to its instruction set (x86-64, ARM64, RISC-V, ...). The C compiler translates your source into machine code ahead of time. That is why a program compiled for x86-64 Linux will not run on an ARM Mac: the bytes mean nothing to a different instruction set. Assembly is a human-readable spelling of machine code; the processor never sees the text.

Exercise 2 · quiz

Memory as numbered boxes

Main memory (RAM) is best pictured as:

Answer
  • A long row of bytes, each with a numeric address

Memory is a huge array of bytes. Each byte has an address, a number starting from 0. Variable names exist only in your source code; the compiler turns each name into an address (or a register). RAM is volatile: it loses everything when power goes away, which is why programs and data live in files on disk between runs.

Exercise 3 · quiz

Compiled versus interpreted

Which statements are true? Select all that apply.

Answer
  • A C program must be compiled before it can run

  • After compiling, the executable can run without the C compiler installed

  • A Python script is normally run by an interpreter program that reads it at run time

Compilation happens once, before running. The result is a standalone executable file; the compiler is not needed to run it. Many errors (wrong types, missing semicolons, unknown names) are caught at compile time, before the program ever runs. Errors such as dividing by zero or reading past the end of an array happen at run time, and in C the compiler usually cannot catch them, which is a big theme of this track.

Exercise 4 · write code

Run a program and change it

The starter is a complete C program that prints Hello from the starter. You do not need to understand every line yet. Press Run to compile and run it, then change the text so the program prints exactly:

Hello from C

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    printf("Hello from the starter\n");
    return 0;
}

Example

expected output

Hello from C
Hint 1

Only the text between the double quotes needs to change.

Hint 2

Keep the \n at the end of the text: it is the newline.

Reference solution
#include <stdio.h>

int main(void)
{
    printf("Hello from C\n");
    return 0;
}