Skip to content
Circuit

C Compilation Process Explained

The command commonly called “compile” often coordinates several tools. A source file can pass through preprocessing, compilation, assembly, and linking before it is ready to execute. Learning what each stage consumes and produces helps you diagnose build failures without treating every message as a syntax error.

A translation unit begins with source

A .c file plus the declarations and other text brought in through its includes forms a translation unit after preprocessing. In a larger project, each source file can be translated separately. Headers normally describe shared interfaces; source files normally provide the corresponding definitions.

For a first experiment, use a single file named main.c that includes stdio.h and calls puts("Ready"). Circuit works with this single-source model. Desktop projects can have many translation units, a build system, external libraries, and target-specific configuration beyond the scope of the browser workspace.

#include <stdio.h>
int main(void) {
    puts("Ready");
    return 0;
}

Preprocessing expands directives

The preprocessor handles header inclusion, conditional compilation, and macro replacement. A missing header can stop progress here before the compiler has checked the function body. Including a header does not automatically mean a matching external library is installed or will be linked.

With a desktop GCC installation, gcc -E main.c writes preprocessed source to standard output. It can be surprisingly large because standard headers contain many declarations. Inspecting it is useful when a macro or #if branch behaves unexpectedly, but beginners rarely need to read every expanded header line.

Compilation produces target assembly

The compiler checks syntax and meaning, then translates the program for a selected target. gcc -S main.c normally writes an assembly file without completing linking. Its contents depend on the target architecture, compiler version, and optimization settings, so copying a particular instruction listing is not a portable C lesson.

At this stage a call to a function can refer to an implementation that will arrive later. A declaration is enough to check the call’s interface. It is not enough to make the final executable complete. This distinction is why a build can pass compilation but fail during linking.

Assembly creates an object file

An assembler converts assembly into an object file containing target code and metadata. gcc -c main.c normally produces main.o while stopping before linking. An object file is a building block; it is not usually a runnable program by itself.

For a multi-file project, each .c file can produce its own object. If only one source file changes, a build system may recompile that part and relink the results. Header changes can affect several translation units, which is why correct dependency tracking matters as projects grow.

Linking resolves the pieces

The linker combines objects, startup support, and needed libraries. gcc main.o -o greeting asks the driver to produce an executable named greeting. Calling a function that has only been declared but never defined commonly produces an undefined-reference diagnostic at this stage.

Multiple definitions are a different problem: two objects may both provide the same externally visible function or variable. Putting ordinary function definitions in a shared header can cause that mistake. Fix ownership of the definition rather than hiding the diagnostic or adding random compiler flags.

/* Desktop shell commands, not C source: */
/* gcc -E main.c           preprocessing */
/* gcc -S main.c           produce assembly */
/* gcc -c main.c           produce an object */
/* gcc main.o -o greeting  link an executable */

Execution is a separate step

On a compatible desktop system, you launch the resulting executable through the shell or an IDE. The runtime arranges startup and eventually calls main. Input files, terminal state, memory availability, and operating-system behavior now matter in ways that were not fully determined during translation.

A zero exit status conventionally signals success. A nonzero status may be an intentional response to invalid input, not necessarily a crash. Conversely, a program can exit with zero and print the wrong answer. Build success, exit success, and algorithm correctness are three different questions.

How the stages map to Circuit

Circuit runs Clang and its linker locally in a Web Worker and targets WebAssembly with WASI support. It does not expose a shell for entering gcc commands. Put only C source in the editor and use Run. The terminal combines compiler diagnostics with the eventual execution output.

The browser target has different environmental capabilities from a native desktop executable. There is no arbitrary access to your disk, processes, or network through ordinary operating-system APIs. If a desktop example depends on those facilities, first determine whether the missing feature is environmental; changing valid C syntax will not add it.