Asking a built compiler what it decided
Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).
Something is wrong. A header resolves to the wrong file, a -l picks up a library
from a directory you have never heard of, a flag you passed appears to do nothing.
The temptation is to read documentation and reason about what should happen.
Don't. A built compiler will tell you what it did, and this chapter is the reference for how to ask. Every chapter after this one leans on these commands, so they come before the mechanisms they observe.
The whole family shares one property, visible in the source: they are handled in a single function that prints an answer and returns before any real work begins.
driver::maybe_print_and_exit () const
{
if (print_search_dirs)
...
if (print_file_name)
...
Listing 3-1: driver::maybe_print_and_exit
(gcc/gcc.cc:8793), called from
gcc.cc:8301.
That is why the manual gives them all the same "and don't do anything else" wording, and why passing two of them at once gets you only the first.
Pick your question
| You want to know | Command |
|---|---|
| What commands would this actually run? | gcc -### foo.c |
Which file would -lfoo or crt1.o resolve to? | gcc -print-file-name=libfoo.a |
| Where does it look for programs and libraries? | gcc -print-search-dirs |
| Where does it look for headers? | gcc -v -E - < /dev/null |
| Is a sysroot in play? | gcc -print-sysroot |
| What was this compiler configured with? | gcc -v |
| Which target is this? | gcc -dumpmachine |
| Which ABI variant am I getting? | gcc -m32 -print-multi-directory |
| Which variants exist at all? | gcc -print-multi-lib |
| Every predefined macro? | gcc -dM -E - < /dev/null |
| The driver's command-line templates? | gcc -dumpspecs |
-print-file-name=: the honest one
$ gcc -print-file-name=libc.a
$ gcc -print-file-name=crt1.o
$ gcc -print-libgcc-file-name # exactly -print-file-name=libgcc.a
This resolves a name through the driver's link-time search path and prints the
absolute result. It is not an approximation of what the linker would find; it
walks the same list the linker is handed as -L flags. -print-libgcc-file-name
is not even a separate mechanism — it is one assignment,
print_file_name = "libgcc.a" (gcc/gcc.cc:4298).
What makes it a diagnostic rather than merely informative is its behaviour on failure, and that behaviour is the entire body of the function:
find_file (const char *name)
{
char *newname = find_a_file (&startfile_prefixes, name, R_OK, true);
return newname ? newname : name;
}
Listing 3-2: find_file (gcc/gcc.cc:8068-8072).
If the file is not found, you get your argument back verbatim. No error, no non-zero exit — just the string you passed in. Which turns one command into a one-line diagnosis, because different files come from different owners:
| Echoed back verbatim | What is wrong |
|---|---|
crt1.o | your sysroot — that file belongs to libc |
crtbegin.o | your prefix or -B — that file belongs to libgcc |
libstdc++.so | libstdc++ was never installed where the driver looks |
If you internalise one thing from this chapter, make it that table. Chapter 1.8 covers who provides which startup file and why they are interleaved.
-print-search-dirs: exactly three lines
$ gcc -print-search-dirs
The output has three lines and no more, because that is literally all the code prints:
printf (_("install: %s%s\n"),
gcc_exec_prefix ? gcc_exec_prefix : standard_exec_prefix,
gcc_exec_prefix ? "" : machine_suffix);
printf (_("programs: %s\n"),
build_search_list (&exec_prefixes, "", false, false));
printf (_("libraries: %s\n"),
build_search_list (&startfile_prefixes, "", false, true));
Listing 3-3: -print-search-dirs (gcc/gcc.cc:8795-8805).
| Line | Backed by | Used to find |
|---|---|---|
install: | gcc_exec_prefix or standard_exec_prefix | — (the resolved install root) |
programs: | exec_prefixes | cc1, as, ld, collect2 — host executables |
libraries: | startfile_prefixes | crt*.o and -l libraries — target files |
There is no
headers:line, and no driver-side option prints header paths. This trips up everyone at least once. The reason is structural, not an oversight: the header search list is built and printed insidecc1, not inside the driver, so no-print-*option ingcchas access to it.
Two more things about Listing 3-3 worth knowing before you read the output.
The sysroot is already folded into the libraries: entries. It is prepended as
the list is built, by add_sysrooted_prefix — you saw that in
Chapter 1.2 — not applied to the output afterwards.
So an entry that does not begin with your sysroot is not sysrooted and never will
be.
And the multilib suffixes are not expanded here. What you see is the raw prefix
list; each entry gets expanded four ways at lookup time, which is
Chapter 1.7's subject. So a directory can appear in
libraries: and still not be the one a particular -m flag actually searches.
The fastest way to find out whether your environment is interfering:
$ gcc -print-search-dirs
$ env -u GCC_EXEC_PREFIX -u COMPILER_PATH -u LIBRARY_PATH gcc -print-search-dirs
Diffing those two settles it in one step. Chapter 1.6 explains what each of those variables does.
The header search list, from cc1
$ gcc -v -E - < /dev/null
Three flags doing three jobs: - reads the program from standard input (which is
empty), -E stops after preprocessing, and -v lets you see cc1's stderr. The
output includes a block like:
#include "..." search starts here:
#include <...> search starts here:
/opt/gcc-arm/lib/gcc/arm-none-eabi/15.2.0/include
...
End of search list.
Those exact strings come from cc1, not the driver:
/* If verbose, print the list of dirs to search. */
if (verbose)
{
fprintf (stderr, _("#include \"...\" search starts here:\n"));
...
fprintf (stderr, _("End of search list.\n"));
Listing 3-4: the verbose printout
(gcc/incpath.cc:391-405).
For C++ you must say so, because standard input has no extension to infer a language from:
$ g++ -v -E -x c++ /dev/null
Check whether the include/c++/... entries begin with your sysroot path. If they
do, this toolchain was configured for sysroot-relative C++ headers; if they do
not, they are prefix-relative and --sysroot will never move them. That
distinction is Chapter 1.12's subject.
If you are on GCC 14 or earlier, this printout has one section fewer. GCC 15 added a fifth include chain for C23's
#embed, somerge_include_chainsnow sysroot-expands quote, bracket, system, after and embed chains (gcc/incpath.cc:359-363) and the verbose output gained an#embed <...> search starts here:block (:408). Confirmed by checking forINC_EMBEDat each release tag: absent through 14.3.0, present at 15.2.0.
When a header resolves to a file you did not expect, the list is not enough — you
want the nesting. -H prints the include tree, one dot of indentation per level:
$ echo '#include <limits.h>' | gcc -H -E -x c - > /dev/null
That is the command that makes #include_next visible, and Chapter 1.9 uses it.
Two related one-liners:
$ gcc -print-file-name=include # GCC's own header directory
$ ls "$(gcc -print-file-name=include)" # what your compiler actually ships
Note what the first one is doing: include is not a library, but
-print-file-name does not care — it looks up whatever name you give it in the
library search list, and GCC's own header directory happens to sit inside a
directory that is on it.
-print-sysroot
$ gcc -print-sysroot
Empty output means no sysroot was configured. That is a meaningful answer, not
a failure, and for a cross compiler it has a further consequence: such a compiler
gets no /lib or /usr/lib search entries at all. Chapter 1.5 shows the single
line of code that decides this, and explains why it is correct rather than
unhelpful.
The manual warns that the printed path comes "possibly with an extra suffix that depends on compilation options", and that suffix is real:
if (print_sysroot)
{
if (target_system_root)
{
if (target_sysroot_suffix)
printf ("%s%s\n", target_system_root, target_sysroot_suffix);
else
printf ("%s\n", target_system_root);
}
Listing 3-5: -print-sysroot (gcc/gcc.cc:8876-8886).
On targets that define SYSROOT_SUFFIX_SPEC — MIPS is the canonical case — the
answer changes with -EL or -msoft-float. Do not cache this value in a
build script. Chapter 1.7 covers the mechanism.
-v: what this compiler was built from
With no source file, gcc -v prints the version, the thread model, and the
complete configure line the compiler was built with. That line is baked in at
build time as a string constant in a generated header
(gcc/gcc.cc:225, printed at :7710) — it
is not re-derived, and it cannot be wrong.
Worth looking for in it:
--with-sysroot— Chapter 1.5--with-specs— see below; it does not show up in-dumpspecs--enable-languages, and any--with-arch/--with-cpudefaults--disable-multilib, which explains a suspiciously short-print-multi-lib
With a source file, -v additionally prints every subprocess command line as it
runs, plus every COLLECT_* variable the driver exports.
-dumpspecs, and the three spec pipelines
$ gcc -dumpspecs
This prints the driver's spec table and exits. Every line beginning with * is a
spec name, and the following lines are its current value:
case OPT_dumpspecs:
{
struct spec_list *sl;
init_spec ();
for (sl = specs; sl; sl = sl->next)
printf ("*%s:\n%s\n\n", sl->name, *(sl->ptr_spec));
if (link_command_spec)
printf ("*link_command:\n%s\n\n", link_command_spec);
exit (0);
}
Listing 3-6: -dumpspecs (gcc/gcc.cc:4216-4226).
Note init_spec () on the second line: the table is initialised, including any
startup rewriting a target does, before being printed. So -dumpspecs shows you
live values, not the macro text in the source. Chapter 1.4 makes a great deal of
that distinction.
What -dumpspecs cannot show you is --with-specs=, and that is by design rather
than an omission. Three different mechanisms are involved:
| Mechanism | What it does | In -dumpspecs? |
|---|---|---|
| built-in specs | the compiled-in table | yes |
--with-specs=… at configure time | rewrites the command line | no |
-specs=FILE at run time | reads a file into the table at startup | not by a plain -dumpspecs |
--with-specs becomes CONFIGURE_SPECS (gcc/configure.ac:1084-1089),
which lands in an array of self specs:
static const char *const driver_self_specs[] = {
"%{fdump-final-insns:-fdump-final-insns=.} %<fdump-final-insns",
DRIVER_SELF_SPECS, CONFIGURE_SPECS, GOMP_SELF_SPECS, GTM_SELF_SPECS,
Listing 3-7: driver_self_specs[] (gcc/gcc.cc:1361-1364),
applied at :8528-8529.
Those rewrite your argument list; they never touch the spec table. So
-dumpspecs genuinely has nothing to show. Use -### to see the effect instead.
Check for a stray
specsfile when a toolchain misbehaves inexplicably. The driver looks for a file literally namedspecson its own library search path (gcc/gcc.cc:8496-8499) and, if it finds one, reads it into the table — silently rewriting the toolchain's behaviour. A default install does not ship one. Find out with:$ ls "$(dirname "$(gcc -print-libgcc-file-name)")"/specs
-dM -E: every predefined macro
$ echo | gcc -dM -E -x c - | sort
-E stops after preprocessing (required for -dM), and -dM replaces the
preprocessed text with a #define for every macro live at the end of the run. It
is parsed one character at a time in the C-family front end:
case 'M': /* Dump macros only. */
Listing 3-8: -dM (gcc/c-family/c-opts.cc:2064, set into
flag_dump_macros at :2068, consumed at
:1547).
Useful narrowings:
$ gcc -dM -E - < /dev/null | grep '__SIZEOF' # every type's size
$ gcc -dM -E - < /dev/null | grep '__GNUC' # compiler version
$ gcc -dM -E - < /dev/null | grep -i 'arm\|thumb' # what your -m flags did
$ gcc -dM -E - < /dev/null | grep __STDC_HOSTED__ # 1 hosted, 0 freestanding
Where those values come from is Part IV's subject.
The multilib family
All four of these resolve against the target's variant table, using the actual
-m* flags on the command line you give them:
| Option | Prints |
|---|---|
-print-multi-directory | GCC-side subdirectory for these flags, . if none |
-print-multi-os-directory | OS-side subdirectory — ., ../lib64, ../lib32, … |
-print-multi-lib | the whole dir;@flag@flag mapping table |
-print-multiarch | the multiarch subdirectory, e.g. x86_64-linux-gnu |
The first two default to . rather than to nothing when there is no variant
(gcc/gcc.cc:8858-8865 and :8887-8894), whereas
-print-multiarch prints an empty line
(:8866-8873) — a small asymmetry that matters if you are consuming the
output from a script.
The useful pairing is a directory question and a file question:
$ gcc -m32 -print-multi-os-directory # which subdirectory
$ gcc -m32 -print-file-name=libc.a # what is actually in it
Because of a fallback covered in Chapter 1.7, those two can disagree in a way that is worth understanding.
-### and -v, one more time
$ gcc -v hello.c # run everything, printing each command line
$ gcc -### hello.c # print the command lines, quoted, run nothing
-### is the one to reach for, because it is safe to run on a broken toolchain
and its output diffs cleanly against a working one. In practice you almost always
want it piped:
$ gcc -### hello.c 2>&1 | grep collect2 # just the link line
$ gcc -### -nostdlib hello.c # confirm what a flag removed
$ gcc -specs=my.specs -### hello.c # confirm what an override did
Documentation coverage
This is one area where the manuals are good. The whole -print-* family is
documented under Overall Options
(gcc/doc/invoke.texi:1549), and -B, --sysroot and the include and library
search options under Directory Options (:19484).
Three things in this chapter are not documented, and are worth knowing you will not find:
- That
-print-search-dirshas noheaders:line, and that no driver option prints header paths at all. You have to notice thatcc1prints them instead. - That an installed
specsfile silently overrides the built-in table. - That
-print-search-dirsshows unexpanded prefixes, so its output is not the set of directories a given-mflag actually searches.
Next: the language those command-line templates are written in.
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.