C Compilation Model Tutorial: From Source Code to Executable

See what GCC does between a readable C file and a running program. Trace one two-file project through every build stage, diagnose failures, and test your understanding.

KnowledgeGate Team

Exam prep & CS education

Updated 11 Sep 20265 min read

A C file is readable text, but the computer eventually runs machine instructions. That gap can feel like one black box, especially when errors mention a preprocessor, compiler, assembler, or linker. Build one file first, then trace the value 1200 through a two-file project from preprocessing to execution. GCC exposes each boundary through a distinct command in a Unix-style shell. Other toolchains may use different commands and suffixes, but the responsibilities remain similar.

Start with the smallest complete C program

Create hello.c:

#include <stdio.h>

int main(void) {
    int units = 3;
    printf("Units: %d\n", units);
    return 0;
}

Build and run it:

gcc -Wall -Wextra -std=c11 hello.c -o hello
./hello

The output is exactly Units: 3. The #include directive is handled before C syntax is compiled. main is the source-level entry function, units stores 3, printf formats the line, and \n ends it. gcc drives several tools, so saying it compiled hello hides the stages below.

Running it proves that your local build path works. The Coding & Skill Development Courses page provides the broader learning route.

Set up the project every stage will trace

Use exactly three files:

/* bill.h */
#ifndef BILL_H
#define BILL_H

#define TAX_RATE 5
int tax_amount(int price);

#endif
/* tax.c */
#include "bill.h"

int tax_amount(int price) {
    return price * TAX_RATE / 100;
}
/* main.c */
#include <stdio.h>
#include "bill.h"

int main(void) {
    int price = 1200;
    int tax = tax_amount(price);

    printf("Base: %d\n", price);
    printf("Tax: %d\n", tax);
    printf("Total: %d\n", price + tax);
    return 0;
}

Build with gcc -Wall -Wextra -std=c11 main.c tax.c -o bill, then run ./bill. The calculation is 1200 * 5 / 100 = 60, followed by 1200 + 60 = 1260:

Base: 1200
Tax: 60
Total: 1260

main.c calls a function defined in tax.c. bill.h gives both sources the same declaration and macro. A header is normally included into a source, not compiled as an independent program. The C Tutorial: 11-Week Path to Pointers and Projects spans the broader learning sequence; this three-file build isolates the intermediate .i, .s, and .o artifacts produced by its compilation stage.

Preprocessing creates the text the compiler sees

Run:

gcc -E main.c -o main.i
gcc -E tax.c -o tax.i

Preprocessing handles # directives, exposes included declarations, and replaces macros. Search the large main.i for main and tax_amount instead of printing expanded <stdio.h>. Find int tax_amount(int price); before the call. In tax.i, find return price * 5 / 100;.

If one source encounters bill.h twice, the first inclusion defines BILL_H and exposes the macro and declaration. The second sees it already defined and skips the guarded block. This is a preprocessing feature, not a linker feature.

Macro replacement is textual. It changes TAX_RATE to 5; it does not calculate the eventual tax of 60.

gcc -E expands main.c and tax.c with bill.h into main.i and tax.i, exposing the tax_amount declaration and replacing TAX_RATE with 5.

Compilation and assembly create object files

Generate assembly from the preprocessed files:

gcc -S main.i -o main.s
gcc -S tax.i -o tax.s

This stage parses tokens, checks syntax and types, and generates target-specific assembly. In main.i, tax_amount accepts and returns int; the call supplies the int named price. Token recognition belongs to a compiler front end; Lexical Analysis in Compiler Design: Tokens to DFA Scanner explains that front-end step. The toolchain then proceeds through preprocessing, compilation, assembly, linking, loading, and execution.

Now assemble both files:

gcc -c main.s -o main.o
gcc -c tax.s -o tax.o

The assembler encodes instructions and metadata into relocatable objects. main.o contains main but still refers to tax_amount and printf; tax.o defines tax_amount. Neither is runnable. The shorthand gcc -c main.c -o main.o performs the first three stages, then stops before linking.

Linking resolves names, then loading starts the program

Link with gcc main.o tax.o -o bill. The tax_amount reference in main.o matches its definition in tax.o. GCC also arranges for printf to resolve from the C runtime libraries. Relocation assigns locations so calls and data references point correctly.

The linker produces bill. Running ./bill makes the operating system load it and required runtime support into a process. Startup code transfers control to main, which calculates 60 and prints the result.

The expanded reconstruction is:

gcc -E main.c -o main.i
gcc -E tax.c -o tax.i
gcc -S main.i -o main.s
gcc -S tax.i -o tax.s
gcc -c main.s -o main.o
gcc -c tax.s -o tax.o
gcc main.o tax.o -o bill

This route and the one-line command have the same result. The long route reveals boundaries; ordinary builds need not expose them.

Both sources pass through preprocessing, compilation, and assembly to objects, link with the C runtime into bill, then run to print 1260.

Diagnose failures by their stage

Cause

Stage and symptom

Fix

bill.h is missing

Preprocessing cannot find the requested file

Restore it or correct the include path

#define TAX_RATE 5;

Compilation sees return price * 5; / 100;

Remove the semicolon and inspect tax.i

Header declares int tax_amount(int price);, source defines long tax_amount(int price)

Compiling tax.c reports conflicting types

Make declaration and definition identical

gcc main.o -o bill omits tax.o

Linking cannot resolve tax_amount

Include tax.o in the link command

Diagnostic wording varies, so identify the stage. A logic bug may still build: price * (TAX_RATE / 100) makes integer division 5 / 100 equal 0, then prints Tax: 0 and Total: 1200. Restore price * TAX_RATE / 100. A runnable executable does not prove its logic.

Test yourself on the compilation model

Try each check before reading its answer.

  1. After gcc -c main.c -o main.o, what ended? Answer: assembly is the last completed stage, and no linked bill has been created.

  2. When does TAX_RATE become 5? Answer: during preprocessing, not linking.

  3. What happens if tax.o is omitted? Answer: linking fails because tax_amount remains unresolved.

  4. Which file defines tax_amount? Answer: tax.c; bill.h only declares it.

Value exercise: Change TAX_RATE to 12 and price to 850. Predict first. Answer: 850 * 12 / 100 = 102, so the output is Base: 850, Tax: 102, Total: 952. This integer calculation happens to divide without a remainder.

Build exercise: After editing only tax.c, what is the shortest separate build? Answer: run gcc -Wall -Wextra -std=c11 -c tax.c -o tax.o, keep main.o, then run gcc main.o tax.o -o bill.

Diagnosis exercise: Change the call to tax_amount(price, 5) while the declaration accepts one argument. Answer: compilation reports an argument-count mismatch before linking.

C compilation model: short version and next step

Remember this chain:

  1. Source files and headers provide text.

  2. Preprocessing handles directives, includes, and macros.

  3. Compilation checks C and generates assembly.

  4. Assembly creates relocatable objects.

  5. Linking resolves cross-file and library symbols into an executable.

  6. Loading creates a running process.

  7. Execution produces the program's behaviour.

main.o can exist while bill does not. bill can run while still containing a logic error.

Build and confirm 1200 to 60 to 1260. Find literal 5 in tax.i. Create both objects without linking and see that no new bill appears, then link and run. Finally try 850 and 12. Change one element at a time so the responsible stage remains visible.

For a structured sequence, continue with the C Language course. If you want to carry these fundamentals into broader coding practice, use Coding for Placements.