What actually runs when you type gcc foo.c
Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).
You type gcc hello.c, and an a.out appears. Somewhere in between, your C was
turned into machine code. The obvious assumption is that gcc did it.
It did not. gcc compiles nothing. It is a driver: a program whose entire
job is to work out which other programs to run, build each one's command line,
and execute them in order. Almost every "GCC can't find X" problem is a driver
problem, and the driver will tell you exactly what it did if you ask it.
Watching it happen
Start with the one flag worth learning before any other:
$ gcc -### hello.c
-### prints every command line the driver would run, fully expanded and
shell-quoted, and then runs none of them. Its sibling -v prints the same
command lines and does execute them. If you only ever remember one thing from
this book, remember -###: it turns "GCC is doing something weird" into a
concrete argument list you can read, diff against a working toolchain, and paste
back into a shell.
What you will see is three or four separate programs, not one:
gcc hello.c
│
├─► cc1 hello.c → /tmp/ccXXXXXX.s the actual C compiler
│ (cc1plus for C++, lto1 for LTO)
├─► as .s → hello.o the assembler, from binutils
│
└─► collect2 hello.o → a.out a wrapper that runs ld
└─► ld
Every arrow there is a real fork and exec. The temporary .s file in the
middle is real too, and you can see the driver invent its name:
$ gcc -### hello.c 2>&1 | grep -o '/tmp/cc[A-Za-z0-9]*\.s'
Nobody named that file. Two separate programs — cc1 and as — nevertheless
agree on it, because the driver generated one name and substituted it into both
command lines. You will meet the mechanism that does this in
Chapter 1.4; for now, note only that the intermediate
files between stages are the driver's bookkeeping, not yours.
You can stop the chain early at each boundary, which is a useful way to convince yourself the stages are really separate:
| Flag | Stops after |
|---|---|
-E | the preprocessor |
-S | cc1, leaving a .s |
-c | as, leaving a .o |
| (none) | the link |
Where those programs live
The driver is not searching your $PATH for cc1. It could not: cc1 is not on
your $PATH, and deliberately so.
| Program | What it is | Installed in |
|---|---|---|
gcc, g++ | the driver | $prefix/bin/ |
cc1, cc1plus, lto1 | the real compilers | $prefix/libexec/gcc/<target>/<version>/ |
collect2, lto-wrapper | GCC's own link-time helpers | same libexec directory |
as, ld | binutils | binutils' install, or the tool directory |
All four rows are host programs — they run on your machine, as processes.
That is the point of libexec: those are private implementation binaries that
happen to need to be exec'd, and versioning them by target and release is what
lets several GCCs coexist.
The driver finds them through a dedicated search list, distinct from the one it uses for libraries. Both are printed by one command:
$ gcc -print-search-dirs
The programs: line is the executable list; the libraries: line is the one
that resolves crt1.o and -lfoo. They are separate lists built by separate
code, and Chapter 1.3 shows you how to read
them. Do not go looking for a third line for headers — there isn't one, for a
reason covered in that chapter.
The driver has no command lines in it
Here is the part that is genuinely surprising: the argument lists you just looked
at are not written in C anywhere. The driver holds templates — strings in a
small %-escape language — and expands them at run time.
Listing 1-1 is the real entry that handles a .c file. This is the whole of
GCC's knowledge about how to compile C, and it is a string constant.
{".c", "@c", 0, 0, 1},
{"@c",
/* cc1 has an integrated ISO C preprocessor. We should invoke the
external preprocessor if -save-temps is given. */
"%{E|M|MM:%(trad_capable_cpp) %(cpp_options) %(cpp_debug_options)}\
%{!E:%{!M:%{!MM:\
...
%{!save-temps*:%{!traditional-cpp:%{!no-integrated-cpp:\
cc1 %(cpp_unique_options) %(cc1_options)}}}\
%{!fsyntax-only:%(invoke_as)}}}}", 0, 0, 1},
Listing 1-1: the .c entry in default_compilers[]
(gcc/gcc.cc:1454-1468). Abridged; the omitted middle handles
-save-temps and rejects -traditional.
Two things fall out of that string immediately.
The literal word cc1 appears in it. That is how the driver knows what to run:
not from a variable, but from the first word of an expanded template. And
%{!fsyntax-only:%(invoke_as)} is why the assembler runs at all. invoke_as is
another template:
static const char *invoke_as =
"%{!fwpa*:\
%{fcompare-debug=*|fdump-final-insns=*:%:compare-debug-dump-opt()}\
%{!S:-o %|.s |\n as %(asm_options) %m.s %A }\
}";
Listing 1-2: invoke_as (gcc/gcc.cc:1313-1322).
Look at |\n as. A newline inside a spec template means "that was the end of
one program; what follows is another program to run." The pipe before it means
what a pipe usually means. So the "chain" in the diagram at the top of this
chapter is not implemented by a loop over stages in the driver's C code — it is
implemented by a newline in a string.
Three consequences matter enough to state now, even though the language itself waits until Chapter 1.4:
- A target customises GCC by replacing a template, not by patching the
driver. Two GCCs built from identical source for different targets can
behave completely differently, because their target headers
#definedifferent templates. -nostdlib,-nostartfilesand-nodefaultlibsare not implemented in C at all. They are conditionals inside one template string. That is exactly why they compose the way they do.- You can replace those templates yourself, per-invocation with
-specs=FILE, without rebuilding anything.
There is no
gcc/specs.cc, and there never has been. The spec language lives entirely insidegcc/gcc.cc, where the interpreter is a function calleddo_spec_1.gcc/specs.ccnevertheless appears in a great deal of secondhand documentation. If you go looking for it you will waste an afternoon.
Why collect2 and not ld
You might have expected the last program in the chain to be ld. On most targets
it isn't; it is a GCC program called collect2, which then runs ld.
The default is set by one macro:
#ifndef LINKER_NAME
#define LINKER_NAME "collect2"
#endif
Listing 1-3: LINKER_NAME (gcc/gcc.cc:902-904).
collect2's own opening comment says what it is for: "Collect static
initialization info into data structures that can be traversed by C++
initialization and finalization routines" (gcc/collect2.cc:1-2).
On platforms whose object format has no native mechanism for running
constructors before main, somebody has to scan the objects, build a table of
constructor and destructor pointers, and get that table into the image.
collect2 does that scan, generates a small C file containing the tables, and
compiles it.
Which means collect2 needs a compiler. So does lto-wrapper, GCC's other
link-time helper, which re-runs code generation at link time for
link-time optimisation and therefore needs both the compiler and your original
options.
This is the recursion that trips people up. If either helper grabbed whatever
gcc happened to be first on $PATH, the second pass would run with a different
prefix, a different sysroot and different search paths than your original
command — and on a cross toolchain, quite possibly a different target.
The driver prevents that by putting the answers in the environment before it
spawns anything. COLLECT_GCC holds the full pathname of the running driver,
taken from argv[0]:
obstack_grow (&collect_obstack, "COLLECT_GCC=", sizeof ("COLLECT_GCC=") - 1);
Listing 1-4: exporting COLLECT_GCC (gcc/gcc.cc:8718).
and COLLECT_GCC_OPTIONS holds every switch, each individually single-quoted, so
the recursion reconstructs the same paths. This is the whole reason -B,
--sysroot and -L survive into the link step. It is also why gcc -v output
is littered with COLLECT_* lines:
$ gcc -v hello.c 2>&1 | grep '^COLLECT_'
Those lines are already shell-quoted, so they paste straight into a terminal.
They are the fastest way to reproduce exactly what collect2 was handed.
The failure mode is worth knowing. lto-wrapper refuses to run without them:
collect_gcc = getenv ("COLLECT_GCC");
if (!collect_gcc)
fatal_error (input_location,
"environment variable %<COLLECT_GCC%> must be set");
Listing 1-5: lto-wrapper hard-errors (gcc/lto-wrapper.cc:1444-1448).
collect2 is more forgiving, and that is the dangerous half:
/* Determine the full path name of the C compiler to use. */
c_file_name = getenv ("COLLECT_GCC");
if (c_file_name == 0)
{
#ifdef CROSS_DIRECTORY_STRUCTURE
c_file_name = concat (target_machine, "-gcc", NULL);
#else
c_file_name = "gcc";
#endif
}
Listing 1-6: collect2's fallback (gcc/collect2.cc:1142-1151).
A collect2 invoked without COLLECT_GCC will silently fall back to
<target>-gcc, and then to plain gcc. On a cross toolchain that can pick up a
different compiler with different startfile and sysroot paths, and the only
symptom is a link that mysteriously resolves the wrong crtbegin.o. Chapter 1.6
covers the whole environment channel.
Two smaller details, both of which surprise people:
The driver will fall back to ld if collect2 is missing, quietly:
/* We'll use ld if we can't find collect2. */
if (! strcmp (linker_name_spec, "collect2"))
{
char *s = find_a_program ("collect2");
if (s == NULL)
set_static_spec_shared (&linker_name_spec, "ld");
}
Listing 1-7: the collect2-to-ld fallback (gcc/gcc.cc:9243-9249).
And when you grep -### output for the link line, grep for collect2, not for
ld — on a normal Linux toolchain ld never appears on the driver's output at
all, because it is collect2 that runs it.
gcc and g++ are the same program
They are not two compilers. They are the same driver binary's worth of code with one extra hook compiled in, and the hook runs before anything else has been acted upon:
/* Do language-specific adjustment/addition of flags. */
lang_specific_driver (&decoded_options, &decoded_options_count,
&added_libraries);
Listing 1-8: the language hook (gcc/gcc.cc:4860-4862).
At that point the command line has been decoded into an array of options but
nothing has been done with it, and the hook is free to insert, delete and reorder
entries. g++'s implementation does exactly two things: it makes .c files
compile as C++, and it appends the C++ runtime libraries to your link — while
silently moving your own -lm and -lc in the process. That is the entire
practical difference between the two commands at link time. Link C++ objects with
gcc and you get undefined std:: symbols; nothing else changes.
Chapter 1.11 takes that hook apart, because the details are surprising and are documented nowhere in the manuals.
A debugging loop that actually works
When something links wrong, run these in order and stop at the first surprise.
$ gcc -### foo.c # is the flag you passed even reaching the linker?
$ gcc -print-search-dirs # is the directory you expect in `libraries:`?
$ gcc -print-file-name=libfoo.a # which copy of it wins?
$ gcc -print-sysroot # is a sysroot in play at all?
$ gcc -print-multi-directory # are you getting the right ABI variant?
The third one is the honest one: -print-file-name walks the same list the
linker is given, so its answer is exactly what the link would pick — and when the
file is not found at all, it echoes your argument straight back. That single
behaviour is the fastest diagnosis in this book, and Chapter 1.3 explains why it
works that way.
What to take from this
gcc is a program that builds command lines. Everything in Part I follows from
that: the search paths exist so the driver can fill in filenames, the spec
language exists so targets can rewrite the command lines, and the environment
variables exist so the driver's own recursive invocations agree with it.
The manuals cover the observation tools well — -###, -v and the -print-*
family are all in invoke.texi under Overall Options
(gcc/doc/invoke.texi:1549), and the spec language has its own node,
Spec Files (:37290). What they do not describe is the
COLLECT_GCC channel from the driver's side, or collect2's fallback when it is
missing. For those, the source above is the documentation.
Next: the vocabulary the rest of Part I is written in — three machine names, and the difference between a program that runs and an object that only ever gets linked.
All source references in this chapter are to GCC 15.2.0
(releases/gcc-15.2.0). Line numbers in other releases will differ; the
surrounding code rarely does. Where behaviour itself changed across a major
version, it is flagged inline.