From Source to Executable
Running gcc hello.c -o hello produces a program that can be run directly, and the process looks like a single step — but gcc is really just a unified front-end command that calls four independent tool stages in sequence behind the scenes. Understanding these four stages makes it much easier to see exactly where a compilation error or a linker error is actually coming from.
Overview of the four stages
Section titled “Overview of the four stages”- Preprocessing: handles preprocessor directives in the source code that start with
#— expanding an#included header file’s contents in place, or substituting a#defined macro with its value. The output at this stage is still text-based code, just with all preprocessor directives resolved - Compilation: translates the preprocessed code into assembly — an intermediate representation tightly coupled to the specific CPU architecture, but still human-readable
- Assembly: translates the assembly code into an object file (a
.ofile), containing machine code that still can’t be run directly — references to external functions and variables inside it are still placeholders, not yet filled in with actual addresses - Linking: combines one or more object files together with the library files they depend on, resolves and fills in the actual addresses for every cross-file function and variable reference, and produces the final, directly runnable executable
Inspecting the intermediate output at each stage
Section titled “Inspecting the intermediate output at each stage”gcc -E hello.c -o hello.i # preprocessing only, output the expanded sourcegcc -S hello.i -o hello.s # compilation only, output assembly codegcc -c hello.s -o hello.o # assembly only, output an object filegcc hello.o -o hello # linking only, output the final executableRunning gcc hello.c -o hello directly is equivalent to automatically running all four steps in sequence, without keeping the intermediate output on disk. Running each stage separately is mainly useful for pinpointing which stage a given problem is actually happening at — a “syntax error,” for instance, usually happens during compilation, while an “undefined reference” error happens during the final linking stage, and the two call for entirely different fixes.
What a header file is for: declaration, not implementation
Section titled “What a header file is for: declaration, not implementation”A header file (a .h file) pulled in via #include typically contains only declarations of functions and variables — stating that a function exists, along with its parameter and return types — rather than their definitions, the actual code that runs inside the function body. The preprocessing stage simply inserts these declarations verbatim into the source, letting the compiler know “this function genuinely exists, and calls to it can be accepted for now.” The actual implementation lives in a separate source file, compiled into its own object file, and the two only get connected during the final linking stage.
Why understanding this pipeline is worth the effort
Section titled “Why understanding this pipeline is worth the effort”Build tools encountered later (Make, covered in the next section), the difference between static and dynamic libraries, and topics like cross-platform compilation are all, at their core, further organization and optimization layered on top of these four stages. Without a grasp of this underlying pipeline, it’s easy to get stuck at “something errored and I don’t know where to even start looking.”
Why this matters
Section titled “Why this matters”This topic is an important part of building a reliable Linux development workflow. Understanding it clearly will make later tasks easier, because it reduces guesswork and helps you recognize when a step is missing or misapplied.
Practical next steps
Section titled “Practical next steps”- Try the commands or configuration shown here in a safe test environment.
- Compare how the concepts apply across different distributions or tools.
- Keep a short note of what worked and what failed so you can diagnose future problems faster.
- Revisit the related article in the series to deepen the connections between topics.
Common pitfalls
Section titled “Common pitfalls”- Skipping verification steps and assuming the system is configured correctly.
- Copying commands without adapting paths, package names, or tool versions for your environment.
- Treating this topic as an isolated tip rather than part of a larger workflow.
Inspecting intermediate files in practice
Section titled “Inspecting intermediate files in practice”A common diagnostic workflow is to stop the compiler at an intermediate stage and inspect the result. For example, if a syntax error is reported during compilation, run gcc -E hello.c -o hello.i and examine the expanded source to see whether macro expansion caused an unexpected token. If a linker error happens instead, inspect hello.o with readelf -s hello.o or compare object files with nm to verify that the expected symbols are actually present.
Typical compiler flags to help diagnose problems
Section titled “Typical compiler flags to help diagnose problems”-Wall -Wextraenables more warnings, catching potential bugs early.-gkeeps debug information in the object files, making runtime debugging possible.-O0disables optimization while debugging, so line numbers and variable values are easier to follow.-fverbose-asmcan be used withgcc -Sto include comments in assembly output.
Practical takeaway
Section titled “Practical takeaway”Understanding how source, assembly, object files, and linking fit together makes build failures much less mysterious. If you can map an error message to one of these four stages, you can choose the right fix instead of guessing.