The compilation process in C turns source code into a program that an execution environment can run. In a conventional native build, that journey has four useful stages: preprocessing, compilation, assembly, and linking. Each stage answers a different question—and leaves different evidence when something goes wrong.
Instead of memorizing four names, we will follow a small order-total calculator from its .c file to its output. You will see what changes at each step, how to keep the intermediate files with GCC, and why “it compiled” is not the same as “it works.” The desktop walkthrough also gives us a useful reference for understanding Circuit’s browser-based build.
C compilation process diagram: source to executable
Read the arrows as “this output becomes the next stage’s input.” The filenames are the ones we will create below. Linking also needs startup code and appropriate library support; your object file is only one part of that final step.
order.c+ included headers such as stdio.h- 01
Preprocessing
Expand includes and macros
order.i - 02
Compilation
Check C and generate assembly
order.s - 03
Assembly
Encode target instructions
order.o - 04
Linking
Connect objects and libraries
order
Additional linking inputs: startup objects + C library support + any other program objects.
orderLoad and runOrder total: 42The word compilation is used in two ways: for the whole build and for the narrower C-to-assembly stage. Also, these four toolchain stages are a practical model, not a complete list of the C standard’s translation phases. Implementations can combine work internally without changing the language rules.
Start with a program whose answer you can predict
Save this as order.c. Six items at a price of seven each should produce a total of 42. The named macro, helper function, and printf call will give us something specific to track through the build.
#include <stdio.h>
#define UNITS 6
int line_total(int units, int price) {
return units * price;
}
int main() {
int total = line_total(UNITS, 7);
printf("Order total: %d\n", total);
return 0;
}With GCC installed and available in your terminal, this command performs the complete native build:
gcc -std=c17 -Wall -Wextra -O0 order.c -o orderOn Linux, run the successful result with ./order. On Windows with GCC, use -o order.exe and run .\order.exe in PowerShell. On macOS, Apple’s toolchain uses Clang; the build concepts still apply. See our C compiler setup guide for VS Code if your terminal cannot find a compiler.
The program should print Order total: 42 followed by a newline. -std=c17 selects the language version, -Wall and -Wextra request useful warnings, and -O0 keeps optimization low for inspection. These are desktop commands; Circuit’s terminal accepts input for your running C program, not shell commands such as gcc.
1. Preprocessing: prepare the C that will be checked
The preprocessor handles directives such as #include, #define, and conditional #if blocks. For this example, it brings in the contents needed from stdio.h and replaces uses of UNITS with the macro’s replacement tokens. A macro is not a variable stored in memory, and expanding it does not execute the multiplication.
gcc -std=c17 -E order.c -o order.iOpen order.i as text and search for line_total(6, 7). That call is the visible result of expanding UNITS. The file may be much larger than order.c because headers contribute declarations and other material. Exact content depends on the compiler’s target and installed headers.
What the prepared source reveals
Your comments normally disappear from this output. You may also see lines beginning with # that name a file and a line number. Those line markers help diagnostics refer back to the original sources; they are not another call to your program. GCC documents this output format in its preprocessor output reference.
After preprocessing, the source together with its included material forms a translation unit. In a project with two .c files, there are normally two translation units. A header can contribute declarations to both without becoming a separately executable program.
Including stdio.h makes the printf interface available to the compiler; it does not mean the header contains the entire machine-code implementation of printf. That distinction will matter at linking. GCC’s macro documentation explains the token-replacement part of this step.
2. Compilation: move from C meaning to target instructions
The compiler parses expressions and declarations, checks the required type and language rules, and generates code for a selected target. In our example, it must understand that line_total multiplies two int values and returns an int. It also checks the calls using the declarations it can see.
gcc -std=c17 -O0 -S order.i -o order.sThe .i extension tells GCC that this source has already been preprocessed. Open order.s in a text editor. Look for labels associated with main and line_total, then notice that C statements have become assembly instructions and assembler directives.
There is no single assembly listing that every reader should get. An x86 compiler and an ARM compiler target different instruction sets. Compiler version, operating system, calling convention, and optimization options also affect the output. Even symbol names may gain a prefix on some platforms.
Why one source line does not equal one instruction
Evaluating a multiplication may involve loading arguments, calculating a result, and returning it. A call can also require placing arguments where the platform expects them. At higher optimization levels, the compiler may inline line_total or simplify the calculation so that no separate multiplication remains on the executed path.
Those changes are a reason to inspect assembly with a specific question, such as “is this function still called?” Reading every instruction is unnecessary for understanding the pipeline. Assembly is still a textual representation; it is not the final executable you launch.
3. Assembly: turn the listing into an object file
The assembler encodes target instructions and data into an object file. In addition to code and data, that file carries information that later tools need, including symbols and relocation records. Think of it as a prepared component of a program, with some connections still to be made.
gcc -c order.s -o order.oorder.o is binary, so opening it as ordinary text will not give you a useful C listing. If your GNU toolchain provides nm, try nm order.o. Look for a definition corresponding to line_total and an external printing-related reference. That reference is often printf; some Windows headers wrap it, so a MinGW build may instead need a helper such as __mingw_vprintf.
An unresolved library reference is expected here. The assembler can record that your code needs a symbol without supplying its implementation. It is the linker’s job to connect that reference later. A .o file is therefore not simply a “smaller executable,” and renaming it to .exe cannot finish the build.
To go straight from source to an object without saving the earlier text files, use gcc -std=c17 -Wall -Wextra -O0 -c order.c -o order.o. The -c option stops before linking. GNU’s assembler output documentation describes the object-file role.
4. Linking: connect objects, startup code, and libraries
The linker combines inputs, resolves symbol references, and performs the relocations needed for the output. Use the GCC driver to link the object; it supplies the normal platform startup and library choices that a direct call to a linker would require you to arrange.
gcc order.o -o orderFor this build, your definition of line_total and the library support for printf now belong to a usable program. Depending on the platform and linking options, library code can be included in the executable or supplied through shared libraries at runtime. Do not assume every executable contains all its dependencies.
Loading is the next event, after a successful build. The operating system prepares the executable and any required runtime dependencies, startup code establishes the C environment, and main is called. Only now does our program calculate a total and print it. The GNU linker’s overview and GCC’s link options distinguish the inputs and available linking choices.
A two-file experiment makes linking visible
In a separate folder, split the same program into these three files. Put the shared declaration in order.h:
#ifndef ORDER_H
#define ORDER_H
int line_total(int units, int price);
#endifPut the calculation’s definition in pricing.c:
#include "order.h"
int line_total(int units, int price) {
return units * price;
}Use this version of order.c. It includes the declaration but no longer defines line_total itself:
#include <stdio.h>
#include "order.h"
#define UNITS 6
int main() {
int total = line_total(UNITS, 7);
printf("Order total: %d\n", total);
return 0;
}gcc -std=c17 -Wall -Wextra -c order.c -o order.o
gcc -std=c17 -Wall -Wextra -c pricing.c -o pricing.o
gcc order.o pricing.o -o orderBoth translation units can compile independently. If you leave pricing.o out of the last command, linking fails because line_total has a declaration but no supplied definition. Adding the missing object fixes that connection. These two-file commands are for a desktop toolchain; Circuit currently runs one source program per build.
When something fails, identify the stage first
“Compiler error” is often used for every problem shown in a build terminal. The message becomes more useful when you ask which input could not become the next output. Start with the earliest diagnostic; later messages can be consequences of the first mistake.
On narrow screens, scroll the table sideways to compare the failure and next action.
| Stage | Example problem | What to check |
|---|---|---|
| Preprocessing | An included header cannot be found. | The header’s name, installation, and include search path. |
| Compilation | A missing semicolon or an undeclared identifier. | The reported location, surrounding syntax, and declarations. |
| Assembly | Invalid instructions in edited assembly. | The assembly syntax and intended target architecture. |
| Linking | line_total is declared but pricing.o is missing. | The supplied objects, definitions, and required libraries. |
| Loading | A required shared library is unavailable. | The runtime dependencies and platform compatibility. |
| Execution | The program prints an incorrect total or never finishes. | The algorithm, input, loop progress, and memory use. |
A useful distinction: duplicate external definitions can also cause a link error. Copying a function body into a header and including it in several .c files is different from sharing its declaration. Our header avoids that mistake; the include guard handles repeated inclusion within a translation unit.
After any failed build, do not launch an old executable and mistake its output for the new source. Successful translation also cannot prove that your algorithm is right or that every pointer is safe. Our guide to common C compiler errors and memory failure guide help separate those cases.
How the compilation process works in Circuit
The desktop diagram describes a native GCC workflow. Circuit uses a downloaded WebAssembly build of LLVM Clang, distributed through YoWASP, to compile and link your C source inside a browser worker. The resulting program targets WebAssembly with WASI support rather than your computer’s native executable format.
The conceptual journey still matters: includes and macros are processed, C is checked and translated, references are linked, and a compatible environment runs the result. The literal .s and .o artifacts in our GCC walkthrough are not files that Circuit’s UI exposes. Circuit executes the linked module with its WASI runtime and connects stdin, stdout, and stderr to the integrated terminal.
You can paste the first, single-file order.c example into the online C compiler and verify the total without installing a desktop toolchain. Source compilation happens on your device; loading the website and compiler assets still requires network requests. The supported environment documentation explains the target, limits, and available features.
Three small experiments that test your understanding
- Track a macro. Change UNITS from 6 to 8. Predict the prepared call in order.i and the new output: 56. Regenerate the files and rebuild before running.
- Separate declaration from definition. In the two-file desktop project, compile both sources successfully, then try linking only order.o. Read the unresolved-symbol diagnostic and restore pricing.o to the link command.
- Find a logic error that builds. Change the multiplication in line_total to addition. The source is valid, but the original inputs now produce 13 instead of 42. Explain why no build stage can infer that you intended a price calculation.
For each experiment, keep the question small: what changed, which stage can observe it, and what artifact or output should differ? That habit turns the compilation diagram into a debugging method. You no longer need to guess whether to edit a declaration, add a missing object, rebuild stale output, or inspect the running algorithm.
To inspect the tools GCC would invoke without running them, try gcc -### order.c -o order. To execute a build while printing the invoked commands, use -v instead. The exact tool names vary by installation. The GCC driver options document these inspection switches and the -E, -S, and -c stopping points used above.