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
| Question | Answered by | Sysroot-aware? |
|---|---|---|
Should -lstdc++ be on the link line? | the g++ driver — Chapter 1.11 | no — pure option rewriting |
Where does #include <vector> come from? | GCC's compiled-in include table | only if configured for it |
Which libstdc++.so does that -l find? | the driver's prefix list → -L flags → ld | yes, 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:
| Artefact | Install 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.m4differs by +70/-5 betweenreleases/gcc-15.2.0and 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 fromgit 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:
| Configuration | include/c++/15 resolves to | Follows --sysroot? |
|---|---|---|
plain cross, no --with-gxx-include-dir | $prefix/$target/include/c++/$ver, relocatable | no |
--with-sysroot=S only | same as above | no |
--with-sysroot=S --with-gxx-include-dir=S/usr/include/c++/15 | <sysroot>/usr/include/c++/15 | yes |
--with-gxx-include-dir=/opt/foo, no overlap with the sysroot | /opt/foo, absolute | no |
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.
| Flag | Moves | Default | Follows --sysroot? |
|---|---|---|---|
--with-gxx-include-dir=DIR | libstdc++ headers | $prefix/$target_alias/include/c++/$ver | only via Listing 12-7 |
--with-gxx-libcxx-include-dir=DIR | libc++ headers, and gates -stdlib= | $prefix/$target_alias/include/c++/v1 | same mechanism |
--enable-version-specific-runtime-libs | headers and libraries into libsubdir | off | never |
--with-toolexeclibdir=DIR | where cross-built libraries install | $tooldir/lib | never |
--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 --helpdoes not hint at it.- That the sysroot decision is per include-directory, carried in the
add_sysrootfield (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--sysrootcannot move them, and it is stated nowhere but the source. - That
--with-toolexeclibdirmoves 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-dirsshows 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.