CHow C Programs Are Built & Run

How C Programs Are Built & Run

When you run a single command like gcc program.c -o program, it feels like one step. In reality, the compiler is orchestrating a pipeline of several distinct stages, each transforming your code a little closer to something the CPU can actually execute. Understanding this pipeline demystifies error messages, explains why C programs need to be recompiled for each platform, and makes concepts like header files and linking make a lot more sense.

The journey from source code to running program
  • Write — you write human-readable source code in a .c file (and possibly .h header files).

  • Preprocess — the preprocessor handles lines starting with #: it expands #include files, replaces #define macros, and resolves #if/#ifdef conditional blocks, producing a single expanded source file.

  • Compile — the compiler translates the preprocessed C code into assembly language for the target CPU architecture, checking your syntax and types along the way.

  • Assemble — an assembler turns that human-readable assembly into an object file (.o/.obj) containing actual machine code, but with some references (like calls to library functions) left unresolved.

  • Link — the linker combines your object file(s) with any libraries you use (like the C standard library) and resolves all the remaining references, producing a single executable file.

  • Execute — the operating system loads that executable into memory and transfers control to it, starting with your main function.

The pipeline at a glance

Stage

Input

Output

What happens

Preprocessing

program.c + headers

Expanded source (in memory / .i file)

#include files are inserted, #define macros are substituted, conditional blocks are resolved.

Compilation

Expanded source

Assembly code (.s file)

Syntax and type checking; source is translated into CPU-specific assembly instructions.

Assembly

Assembly code

Object file (.o / .obj)

Assembly mnemonics become raw machine code, with placeholders for unresolved symbols.

Linking

Object file(s) + libraries

Executable file

Placeholder references (like printf) are resolved to actual code from libraries.

Execution

Executable file

Running process

The OS loads the executable into memory and jumps to its entry point.

Seeing the stages yourself

Most compilers let you stop the pipeline early and inspect the intermediate output at each stage, which is a great way to see this process concretely rather than just reading about it.

Bash
# Stop after preprocessing — see the fully expanded source
gcc -E program.c -o program.i

# Stop after compilation — see the generated assembly
gcc -S program.c -o program.s

# Stop after assembling — produce an object file only
gcc -c program.c -o program.o

# Run the full pipeline through linking — produce the final executable
gcc program.c -o program
Why C is a compiled language

C is a compiled language, which means the translation from source code to machine code happens once, ahead of time, before the program ever runs. This is different from an interpreted language like Python or JavaScript, where an interpreter reads and executes your source code (or a close representation of it) line by line every time the program runs.

Compiled (C)

Interpreted (typical scripting language)

When translation happens

Once, ahead of time, producing a standalone executable.

Every time the program runs, as it executes.

Runtime dependency

None — the executable contains native machine code and runs directly.

Requires an interpreter to be installed and running alongside the program.

Typical startup speed

Fast — the OS just loads machine code and starts executing.

Slower — the interpreter has to parse and set up execution before your code runs.

Portability of the built artifact

The compiled executable is tied to one CPU architecture and OS.

The same source file can run anywhere the interpreter is available.

"Compile once per platform"

Because a C compiler translates your source directly into machine code for one specific combination of CPU architecture and operating system, an executable built on a 64-bit Linux machine will not run on Windows, and an executable built for an ARM-based device will not run on an x86 machine. This is what people mean when they say C is portable at the source level but not at the binary level: the same .c file can be recompiled to run correctly on many different platforms, but a single compiled executable only runs on the platform it was built for.

Source portability vs binary portability
Writing standard, portable C (avoiding compiler-specific extensions and platform-specific assumptions) means your source code can be recompiled for a new platform with little or no changes. That is very different from a compiled binary itself being portable — it never is, which is why software is distributed either as source code to be compiled locally, or as separate pre-built binaries for each target platform.