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 (processor) executes instructions: tiny operations like "add these two numbers", "copy 8 bytes from memory address 4096 into a register", "if the last result was zero, jump to instruction X". It holds a handful of very fast storage slots called registers, and a special register, the program counter, which holds the address of the next instruction.
- Memory (RAM) is a long row of bytes. A byte is 8 bits, enough to store a number from 0 to 255. Every byte has an address: a plain number, starting at 0. When people say a laptop has 16 GB of RAM, they mean roughly 16 billion addressable bytes. Memory is fast but forgets everything when power is lost.
- Storage (disk, SSD) keeps files when the power is off. It is far slower than RAM, so programs are loaded from disk into memory before they run.
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:
- Translation happens before the program runs. The compiler reads the whole file, checks it, and produces machine code. If the source has an error the compiler can detect, like a missing semicolon or a misspelled name, no executable is produced at all.
- The executable is standalone. You can copy it to another Linux machine with the same kind of processor and run it there without a compiler.
- The same source can be compiled for different processors. That portability is the original point of C: Unix was rewritten in C so it could be moved to new hardware by writing a new compiler instead of rewriting the operating system.
- The compiler cannot catch everything. It can check that you add numbers to numbers. It generally cannot know whether an array index will be out of range when the program runs with some particular input. In C, many such mistakes are not reported at all; the program just does something wrong. We will spend real time on how to avoid and detect these.
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:
- The shell asks the operating system kernel to start a new process (a running instance of a program) and load the file
hellointo it. - 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.
- The kernel points the CPU at the program's entry point. A little startup code from the C library runs first, then calls your
mainfunction. - 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.
- When
mainreturns, 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
- "The computer runs my C code." It runs the machine code the compiler produced from it. When something is confusing, ask what the machine code is doing with memory.
- "If it compiled, it is correct." Compiling only means the source followed the grammar and type rules. Logic errors and memory errors show up at run time, or never visibly.
- "Variables have names in memory." Names are for humans and the compiler. In the running program there are only addresses and bytes.
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
- The CPU executes machine code: numeric instructions specific to its instruction set, in a fetch-decode-execute loop.
- Memory is a row of numbered bytes; variable names do not exist at run time.
- C source is translated ahead of time by a compiler into a standalone executable.
- Running a program creates a process with regions for code, globals, the stack and the heap;
mainreturns an exit status to whoever started it. - C trusts the programmer: no bounds checks, manual memory, undefined behavior for many mistakes. Good habits and tools make up for it.
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.