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 knowCommand
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 verbatimWhat is wrong
crt1.oyour sysroot — that file belongs to libc
crtbegin.oyour prefix or -B — that file belongs to libgcc
libstdc++.solibstdc++ 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).

LineBacked byUsed to find
install:gcc_exec_prefix or standard_exec_prefix— (the resolved install root)
programs:exec_prefixescc1, as, ld, collect2host executables
libraries:startfile_prefixescrt*.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 inside cc1, not inside the driver, so no -print-* option in gcc has 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, so merge_include_chains now 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 for INC_EMBED at 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-cpu defaults
  • --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:

MechanismWhat it doesIn -dumpspecs?
built-in specsthe compiled-in tableyes
--with-specs=… at configure timerewrites the command lineno
-specs=FILE at run timereads a file into the table at startupnot 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 specs file when a toolchain misbehaves inexplicably. The driver looks for a file literally named specs on 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:

OptionPrints
-print-multi-directoryGCC-side subdirectory for these flags, . if none
-print-multi-os-directoryOS-side subdirectory — ., ../lib64, ../lib32, …
-print-multi-libthe whole dir;@flag@flag mapping table
-print-multiarchthe 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-dirs has no headers: line, and that no driver option prints header paths at all. You have to notice that cc1 prints them instead.
  • That an installed specs file silently overrides the built-in table.
  • That -print-search-dirs shows unexpanded prefixes, so its output is not the set of directories a given -m flag 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.