How C Programs Are Built

"Compiling" a C program is really four steps: the preprocessor expands #include and #define, the compiler translates C into assembly, the assembler produces machine code, and the linker joins it with libraries into an executable. Knowing the steps makes error messages — and multi-file projects later — far less mysterious.

The four stages

TXT
 main.c ──▶ preprocessor ──▶ compiler ──▶ assembler ──▶ main.o ──▶ linker ──▶ main (executable)
           (#include,        (C → assembly) (assembly →             (+ C library,
            #define)                         machine code)            other .o files)
StageInput → outputSee it with
Preprocess.c → expanded C sourcegcc -E main.c
CompileC → assembly (.s)gcc -S main.c
Assembleassembly → object file (.o)gcc -c main.c
Linkobject files + libraries → executablegcc main.o -o main

Normally one command runs all four. You can stop after any stage to look at the intermediate result.

The preprocessor

The preprocessor is a text-substitution step. #include <stdio.h> is literally replaced by the contents of the stdio.h header file, and #define names are replaced by their values:

C
#include <stdio.h>
#define GREETING "Hi there"
#define REPEAT 3

int main(void) {
    for (int i = 0; i < REPEAT; i++) {
        printf("%s\n", GREETING);
    }
    return 0;
}
Output
Hi there
Hi there
Hi there

After preprocessing, the compiler never sees GREETING or REPEAT, only "Hi there" and 3. Lesson 51 covers the preprocessor in depth.

Headers and libraries

A header (.h file) contains declarations: it tells the compiler that a function such as printf exists and what arguments it takes. The function's actual machine code lives in a library — for printf, the C standard library, which the linker adds automatically. A few libraries need asking for, like the maths library on Linux (-lm).

That split explains two different kinds of error:

  • Compiler errors — your C is invalid, or you used a function without its header.
  • Linker errors ("undefined reference to …", or "undefined symbol" on macOS) — the code compiled, but the linker can't find the function's definition: a missing library or a missing .c file.

Errors and warnings

An error stops compilation. A warning means the code is valid C but probably wrong — and in C, "valid but wrong" is the dangerous kind. Here, the comparison uses = (assignment) instead of ==:

C
#include <stdio.h>

int main(void) {
    int stock = 0;
    if (stock = 5) {
        printf("In stock\n");
    }
    return 0;
}
Compiler warnings
main.c:5:15: warning: using the result of an assignment as a condition without parentheses [-Wparentheses]

This program would compile and run — and always print "In stock". Treat every warning as a bug to fix. Many projects add -Werror, which turns warnings into errors.

Compiled vs. interpreted

Python or JavaScript programs are run by an interpreter that reads the source every time. A C program is translated once, ahead of time, into machine code for one kind of processor and operating system. That's why C is fast and needs no runtime installed — and also why you must recompile after every change, and compile separately for each platform.

Try it yourself

Compile this with gcc -E main.c | tail -5 to see the preprocessed end of the file, then with gcc -S main.c and open main.s to see the assembly the compiler generated.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
With tax: 60