Which libstdc++, and which <vector>

Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).

You have a cross toolchain and a target root filesystem. You pass --sysroot=/arm-rootfs, you point it at a rootfs that definitely contains /usr/include/c++/15 and /usr/lib/libstdc++.so.6, and g++ -v -E -x c++ /dev/null shows you a <vector> from somewhere else entirely — a path under your compiler's install prefix. Then you try to fix it with -B and nothing changes at all.

Neither half of libstdc++ is in the sysroot, by design. The headers and the library both live relative to where the compiler is installed, and --sysroot is about the C library. There is exactly one configure-time route by which the headers become sysroot-relative, and you have to ask for it.

Chapter 1.11 got -lstdc++ onto the link line. This chapter answers which file it resolves to, and which <vector> you compiled against — two questions with two completely different mechanisms that happen to be moved by the same prefix.

Three questions people conflate

QuestionAnswered bySysroot-aware?
Should -lstdc++ be on the link line?the g++ driver — Chapter 1.11no — pure option rewriting
Where does #include <vector> come from?GCC's compiled-in include tableonly if configured for it
Which libstdc++.so does that -l find?the driver's prefix list → -L flags → ldyes, but the sysroot usually loses

The third row carries Chapter 1.8's rule: the driver never opens libstdc++.so. It emits one -l and a pile of -L, and ld resolves. So "which library" is really "which directory comes first".

Where a cross install actually puts it

For a cross — host != target — libstdc++ installs into the tool directory, not the sysroot and not $prefix/lib:

ArtefactInstall location
Headers$prefix/$target_alias/include/c++/$version
libstdc++.{a,so}$exec_prefix/$target_alias/lib[/$multi_os_dir]

Both come out of one autoconf macro, GLIBCXX_EXPORT_INSTALL_INFO (libstdc++-v3/acinclude.m4:729). The headers first:

  # Default case for install directory for include files.
  if test $version_specific_libs = no && test $gxx_include_dir = no; then
    gxx_include_dir='include/c++/${gcc_version}'
    if test -n "$with_cross_host" &&
       test x"$with_cross_host" != x"no"; then
      gxx_include_dir='${prefix}/${target_alias}/'"$gxx_include_dir"
    else
      gxx_include_dir='${prefix}/'"$gxx_include_dir"
    fi
  fi

Listing 12-1: the header install directory (libstdc++-v3/acinclude.m4:760-769).

Then the library, whose comment states the policy outright:

  # Calculate glibcxx_toolexecdir, glibcxx_toolexeclibdir
  # Install a library built with a cross compiler in tooldir, not libdir.
  if test x"$glibcxx_toolexecdir" = x"no"; then
    if test -n "$with_cross_host" &&
       test x"$with_cross_host" != x"no"; then
      glibcxx_toolexecdir='${exec_prefix}/${host_alias}'
      case ${with_toolexeclibdir} in
	no)
	  glibcxx_toolexeclibdir='${toolexecdir}/lib'
	  ;;

Listing 12-2: the library install directory (libstdc++-v3/acinclude.m4:784-793).

Read this file at the tag, not in a checkout. libstdc++-v3/acinclude.m4 differs by +70/-5 between releases/gcc-15.2.0 and the AdaCore working branch in the trees this book was written from, and by more between point releases. Every line number in this section was re-derived from git show releases/gcc-15.2.0:libstdc++-v3/acinclude.m4; taking them from a worktree puts them roughly two dozen lines out.

Two traps in those two listings

Both are the kind of thing you only notice after a build has gone wrong.

$prefix and $exec_prefix are different variables. They default to the same value, which is why nobody notices. But Listing 12-1 builds the header path from ${prefix} and Listing 12-2 builds the library path from ${exec_prefix}. Pass --exec-prefix and your C++ headers and your C++ library land in different trees, from one configure run, with no warning.

${host_alias} in a target library means GCC's target. Target libraries are configured with --host=<target triple>, because from libstdc++'s own point of view the machine it will run on is GCC's target. So ${exec_prefix}/${host_alias} in Listing 12-2 is $exec_prefix/<target>, the tooldir. libgcc plays the same trick with real_host_noncanonical. Whenever a path formula inside a target library says "host", read "GCC's target" — Chapter 1.2's three machines, seen from the other end.

The driver's matching view

The install rules would be useless if the driver looked elsewhere, so the same two paths are computed a second time, in gcc/configure.ac. That file says so:

# This logic must match libstdc++-v3/acinclude.m4:GLIBCXX_EXPORT_INSTALL_INFO.
if test x${gcc_gxx_include_dir} = x; then
  if test x${enable_version_specific_runtime_libs} = xyes; then
    gcc_gxx_include_dir='${libsubdir}/include/c++'
  else
    libstdcxx_incdir='include/c++/$(version)'
    if test x$host != x$target; then
       libstdcxx_incdir="$target_alias/$libstdcxx_incdir"
    fi
    gcc_gxx_include_dir="\$(libsubdir)/\$(libsubdir_to_prefix)$libstdcxx_incdir"
  fi

Listing 12-3: the driver's half (gcc/configure.ac:213-223).

It is worse than a duplicated formula: libstdc++'s copy of the comment (acinclude.m4:727-728) names a third file, config/gxx-include-dir.m4, that must be kept consistent too. Change one, change all three, or the headers install where the driver will not look.

For the library side the driver does not compute a path at all — it adds the whole tooldir to its prefix list:

  add_prefix (&startfile_prefixes,
	      concat (tooldir_prefix, "lib", dir_separator_str, NULL),
	      "BINUTILS", PREFIX_PRIORITY_LAST, 0, 1);

Listing 12-4: gcc/gcc.cc:5537-5539.

That is add_prefix, not add_sysrooted_prefix. One function call is the whole reason --sysroot does not move your C++ library. Compare Chapter 1.5's Listing 5-9: the sysrooted variant prepends target_system_root (:3177-3205); this one does not.

Does --sysroot move the headers? Only if you asked

Each entry in the compiled-in include table carries its own sysroot flag — not a global setting, a per-directory one:

  const char add_sysroot;	/* FNAME should be prefixed by
				   cpp_SYSROOT.  */

Listing 12-5: gcc/cppdefault.h:48-49.

and for the C++ directories the flag is a configure-time macro:

#ifdef GPLUSPLUS_INCLUDE_DIR
    /* Pick up GNU C++ generic include files.  */
    { GPLUSPLUS_INCLUDE_DIR, "G++", 1, 1,
      GPLUSPLUS_INCLUDE_DIR_ADD_SYSROOT, 0 },
#endif

Listing 12-6: gcc/cppdefault.cc:43-47.

Because the decision is baked into the table per entry, no command-line flag can change it after the fact. --sysroot supplies the string; whether a given directory uses it was decided when GCC was configured.

That macro comes from gcc_gxx_include_dir_add_sysroot, which is initialised to zero at gcc/configure.ac:211 and raised in exactly one place:

elif test "${with_sysroot+set}" = set; then
  gcc_gxx_without_sysroot=`expr "${gcc_gxx_include_dir}" : "${with_sysroot}"'\(.*\)'`
  if test "${gcc_gxx_without_sysroot}"; then
    gcc_gxx_include_dir="${gcc_gxx_without_sysroot}"
    gcc_gxx_include_dir_add_sysroot=1
  fi
fi

Listing 12-7: gcc/configure.ac:224-230.

Read the elif. It is the else-branch of if test x${gcc_gxx_include_dir} = x from Listing 12-3, so it is reachable only when you passed --with-gxx-include-dir. If you did not, the default branch runs, the path is prefix-relative, and the flag stays 0 forever. Even when you did pass it, the sysroot is only factored out if your path is literally string-prefixed by --with-sysroot's value — that expr is a string match, not a path comparison.

Which gives the whole case table:

Configurationinclude/c++/15 resolves toFollows --sysroot?
plain cross, no --with-gxx-include-dir$prefix/$target/include/c++/$ver, relocatableno
--with-sysroot=S onlysame as aboveno
--with-sysroot=S --with-gxx-include-dir=S/usr/include/c++/15<sysroot>/usr/include/c++/15yes
--with-gxx-include-dir=/opt/foo, no overlap with the sysroot/opt/foo, absoluteno

Only the third row gives you the sysroot-native layout, and it needs both options with one path textually inside the other. The default buys you something else that is usually worth more: relocatability.

What the default gives you instead

If a standard directory is not sysrooted, add_standard_paths takes a different branch — it relocates the path against the installed prefix:

	  else if (!p->add_sysroot && relocated
		   && !filename_ncmp (p->fname, cpp_PREFIX, cpp_PREFIX_len))
	    {
 	      static const char *relocated_prefix;
	      char *ostr;
	      /* If this path starts with the configure-time prefix,
		 but the compiler has been relocated, replace it
		 with the run-time prefix.  The run-time exec prefix
		 is GCC_EXEC_PREFIX.  Compute the path from there back
		 to the toplevel prefix.  */

Listing 12-8: gcc/incpath.cc:192-201.

This is the branch a normal cross takes: not sysrooted, but relocated — move the installed toolchain and the C++ headers follow it, because the path is recomputed from GCC_EXEC_PREFIX (Chapter 1.6) rather than being an absolute string. The two branches are mutually exclusive by construction, which is the precise reason --sysroot cannot move GCC's own header directories: they are not sysroot-relative, they are prefix-relative, and the prefix is discovered at run time.

-B cannot help either. -B feeds include_prefixes, and %I turns that into -isystem <B>/<target>/<version>/include and .../include-fixed — GCC's own header directories, from Chapter 1.9. The C++ directories are not in that set. So -B changes which <stdint.h> you get and not which <vector>.

The per-invocation escape hatch

Independent of all the configure-time machinery, a leading = or $SYSROOT on any -I, -isystem or -idirafter is expanded to the sysroot:

      if (p->user_supplied_p)
	{
	  if (p->name[0] == '=')
	    p->name = concat (sysroot, p->name + 1, NULL);
	  if (startswith (p->name, "$SYSROOT"))
	    p->name = concat (sysroot, p->name + strlen ("$SYSROOT"), NULL);
	}

Listing 12-9: gcc/incpath.cc:333-339; applied to the quote, bracket, system, after and #embed chains at :356-364.

Note user_supplied_p: this only ever touches paths you passed on the command line, never the compiled-in table. Which makes it the right tool for testing a sysroot-resident header set against a compiler that was not configured for one:

$ arm-linux-gnueabihf-g++ --sysroot=/arm-rootfs \
    -nostdinc++ -isystem =/usr/include/c++/15 -E -v -x c++ /dev/null

Listing 12-10: -nostdinc++ drops the compiled-in C++ directories (Chapter 1.10), = supplies sysroot-relative ones.

Does --sysroot move the library? Yes, and it usually loses

Two mechanisms put the sysroot into the link-time search:

Sysrooted startfile prefixes. /lib/ and /usr/lib/ (gcc/gcc.cc:1613-1617) are added through add_sysrooted_prefix (:8621-8630), which prepends target_system_root.

The linker is told. If ld supports it, %(sysroot_spec) — that is --sysroot=%R (:1189-1191) — is prepended to the whole link spec (:8554-8564), which makes ld's own built-in search directories and any =-prefixed path in a linker script sysroot-relative too.

So the sysroot genuinely is searched. It loses because Listing 12-4's tooldir prefix is not sysrooted and is where the file actually is. A sysroot copy only wins if you put one there and it sorts earlier — at which point you have two candidates and a search-order question you now have to answer. Usually the wrong move.

There is one gate worth knowing about here, which explains a different confusion:

  else if (*cross_compile == '0' || target_system_root)

Listing 12-11: gcc/gcc.cc:8590.

A cross compiler configured with no sysroot at all gets no /lib or /usr/lib prefixes whatsoever. That is not a bug: searching the host's /usr/lib for target libraries is how you get "file in wrong format" errors. If a cross seems to have suspiciously few library paths, this one line is why, and it is correct. Ports defining STARTFILE_PREFIX_SPEC take the earlier branch instead (:8579-8587), which wins outright over this whole chain.

The four configure flags that move it

Each has to be honoured twice — by libstdc++'s install rules and by the driver's compiled-in defaults.

FlagMovesDefaultFollows --sysroot?
--with-gxx-include-dir=DIRlibstdc++ headers$prefix/$target_alias/include/c++/$veronly via Listing 12-7
--with-gxx-libcxx-include-dir=DIRlibc++ headers, and gates -stdlib=$prefix/$target_alias/include/c++/v1same mechanism
--enable-version-specific-runtime-libsheaders and libraries into libsubdiroffnever
--with-toolexeclibdir=DIRwhere cross-built libraries install$tooldir/libnever

--with-gxx-libcxx-include-dir does double duty. Besides naming the libc++ header directory, it decides whether -stdlib= exists at all: =no disables the option, a path enables it, and unset enables it only on recent Darwin (gcc/configure.ac:255-274). If -stdlib=libc++ is rejected as an unknown option, that is why — and Chapter 1.11's -lc++/-lc++abi emission is unreachable without it. The libc++ header entry is a separate row in the same table, distinguished by cplusplus == 2 (cppdefault.cc:58-62) and selected by flag_stdlib_kind at incpath.cc:176-177.

--enable-version-specific-runtime-libs moves both halves out of the tooldir into GCC's versioned directory — headers to ${libsubdir}/include/c++, libraries to ${libdir}/gcc/${host_alias}/${gcc_version}$(MULTISUBDIR) (acinclude.m4:771-782). Two things follow: it applies only if you did not pass --with-gxx-include-dir (read the && in Listing 12-1), and a version-specific layout can never be sysroot-relative, because Listing 12-7 is in the branch it does not take.

--with-toolexeclibdir moves the install, not the search. This is the one that produces a working build and a broken compiler. The flag is consumed at acinclude.m4:790-797, so the library installs where you said. But Listing 12-4 builds the driver's prefix from tooldir_prefix and the target triple — it is not parameterised by this flag. Point it somewhere unusual and g++ will install libstdc++.so there and then fail to find it, unless you also supply -L or -B.

Asking your own compiler

Every claim above is checkable on an installed toolchain, and this is the sequence to run when a link picks up the wrong C++ runtime:

$ arm-linux-gnueabihf-g++ -print-sysroot                  # empty means: none configured
$ arm-linux-gnueabihf-g++ -print-search-dirs              # raw prefix lists, pre-expansion
$ arm-linux-gnueabihf-g++ -print-file-name=libstdc++.so   # which file the -l resolves to
$ arm-linux-gnueabihf-g++ -print-file-name=libstdc++.a    # ditto for -static-libstdc++
$ arm-linux-gnueabihf-g++ -mcpu=cortex-a9 -print-multi-os-directory
$ arm-linux-gnueabihf-g++ -v -E -x c++ /dev/null          # the C++ header search list

Listing 12-12: the six questions, in the order you should ask them.

Two readings to remember. -print-file-name echoes its argument back unchanged when the file was not found (Chapter 1.3), so libstdc++.so coming back verbatim means "not found", not "found in the current directory". And in the last command, check whether the include/c++/… lines begin with your sysroot path: if they do you have the third row of the case table, and if they do not, --sysroot will never move them and no amount of retrying will change that.

-print-search-dirs is the one to be careful with. It prints prefixes before multilib expansion, so its output is not the set of directories any particular -mcpu actually searches — Chapter 1.7's four-way expansion happens later, inside for_each_path (gcc/gcc.cc:2778) with the variant chosen by set_multilib_dir (:9764). -print-file-name is the honest answer, because it performs the whole search.

Documentation coverage

The four configure flags are all in gcc/doc/install.texi, and the search-path ordering is in the internals manual (info gccint 'Target Macros' Driver, or gcc/doc/tm.texi, "Here is the order of prefixes tried for startfiles").

Not documented anywhere:

  • --with-gxx-include-dir's interaction with --with-sysroot. The entire case table above exists only as the shell in Listing 12-7. configure --help does not hint at it.
  • That the sysroot decision is per include-directory, carried in the add_sysroot field (Listing 12-5) and therefore unchangeable by any flag. The corollary — that GCC's own directories are relocated rather than sysrooted (Listing 12-8) — is the precise reason --sysroot cannot move them, and it is stated nowhere but the source.
  • That --with-toolexeclibdir moves the install without moving the search. The option is documented; the asymmetry is not.
  • That the path logic is duplicated across three files. Only the in-tree comments say so, and they are the only warning you get.
  • That -print-search-dirs shows prefixes before multilib expansion.

Things that surprise people

--sysroot does not move your C++ headers unless you configured --with-gxx-include-dir with a path inside --with-sysroot.

$prefix/$target/lib is not sysrooted, is where a cross libstdc++ actually lives, and is searched regardless of --sysroot.

-B does not move C++ header search. It moves GCC's own headers, which is enough to change <stdint.h> and not <vector>.

A cross ignores LIBRARY_PATH. The guard is *cross_compile == '0' (Chapter 1.6). Do not debug a cross with it.

A successful link says nothing about whether the program runs. Link-time search and run-time search are unrelated systems: -L and the prefix lists resolve the -l; DT_NEEDED, DT_RUNPATH, ld.so.conf and LD_LIBRARY_PATH decide whether the target finds libstdc++.so.6 at exec time. Getting the first right tells you nothing about the second.


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. libstdc++-v3/acinclude.m4 in particular moves between point releases and in vendor forks — read it at the tag. Where behaviour itself changed across a major version, it is flagged inline.