What actually reaches the linker
Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).
You have looked at a -v link line before, and you have probably wondered about
two things in it. There are five crt-something object files, interleaved around
your own objects in an order nobody explained. And -lgcc appears twice.
Neither is an accident, and neither is arbitrary. This chapter is the canonical account of what reaches the linker, in what order, and who supplies each piece.
The shape of a link
For a glibc target, the objects come out in this order:
crt1.o crti.o crtbegin.o …your objects… crtend.o crtn.o
└────── libc ─────┘└ libgcc ┘ └ libgcc ┘ └ libc ┘
and then the libraries:
-lgcc -lc -lgcc
Two facts about that picture explain most link problems you will ever have with GCC:
- The startup files come from two unrelated providers, interleaved.
- libgcc appears twice, on purpose.
See it for yourself, on your own toolchain:
$ gcc -### hello.c 2>&1 | grep collect2
Grep for collect2, not ld — Chapter 1.1 explains why.
It is one template
Before the pieces, the thing they sit in. The entire link is a single spec, and its structure is the link order:
%{!fsyntax-only:%{!c:%{!M:%{!MM:%{!E:%{!S: ← only when actually linking
%(linker) ← collect2 (or ld)
%{flto} %{fno-lto} %{flto=*} %l ← target link flags
%X %{o*} %{e*} … ← -Wl, options, -o, -e …
%{!nostdlib:%{!r:%{!nostartfiles:%S}}} ← crt1.o crti.o crtbegin.o
%{static|no-pie|static-pie:} %@{L*} %(link_libgcc)
%o ← YOUR object files
%{!nostdlib:%{!r:%{!nodefaultlibs:
%(link_ssp) %(link_gcc_c_sequence)}}} ← libgcc + libc + libgcc
%{!nostdlib:%{!r:%{!nostartfiles:%E}}} ← crtend.o crtn.o
%{T*} \n%(post_link) }}}}}}
Listing 8-1: LINK_COMMAND_SPEC, abridged
(gcc/gcc.cc:1159-1178).
Three observations, one of which is the whole reason this chapter can be short.
Position in the template is position on the command line. %S … %o …
libraries … %E is the link order. The startfile/endfile split exists for no
other reason than that %S and %E sit on opposite sides of %o.
-nostdlib, -nostartfiles, -nodefaultlibs and -r are pure conditionals.
No C code implements them; they fail a %{!…:} guard and the text simply is not
emitted. The three guards live at gcc.cc:1168,
:1176 and :1177, and
Chapter 1.10 reads the truth table
straight off the nesting.
A target customises one small sub-spec, not this string. Nobody copies Listing 8-1.
The %(link_libgcc) reference just before your objects is worth a note, since it
is where all of Chapter 1.5's work shows up:
#ifndef LINK_LIBGCC_SPEC
/* Generate -L options for startfile prefix list. */
# define LINK_LIBGCC_SPEC "%D"
#endif
Listing 8-2: where the -L flags come from
(gcc/gcc.cc:1180-1183).
One %D. That is the entire connection between the driver's prefix list and the
linker's search path — one directive, expanding to one -L per directory.
Two providers, one search list
| Files | Provider | Installed under | Moved by |
|---|---|---|---|
crt1.o, Scrt1.o, gcrt1.o, crti.o, crtn.o | the C library | the sysroot's /usr/lib | --with-sysroot, --sysroot |
crtbegin*.o, crtend*.o | GCC's libgcc | $prefix/lib/gcc/<t>/<v>/ | --prefix, -B |
Both halves are written identically in the spec, because %s means nothing more
than "look this name up in the library search list" (Listing 4-9). One list, two
origins, and the difference invisible in the spec text.
The libc half is found because /lib/ and /usr/lib/ are registered with
add_sysrooted_prefix — Listing 5-9 in Chapter 1.5. That call is where the
sysroot gets glued on. The libgcc half is found through prefixes derived from the
driver's own location and is never sysrooted.
So the diagnosis is one pair of commands:
$ gcc -print-file-name=crt1.o # the libc half
$ gcc -print-file-name=crtbegin.o # the libgcc half
| Echoed back verbatim | Means |
|---|---|
crt1.o | your sysroot is wrong |
crtbegin.o | your prefix or -B is wrong |
What a real startfile spec looks like
The default STARTFILE_SPEC in the driver is not the crt1/crti/crtbegin shape at
all:
/* config.h can define STARTFILE_SPEC to override the default crt0 files. */
#ifndef STARTFILE_SPEC
#define STARTFILE_SPEC \
"%{!shared:%{pg:gcrt0%O%s}%{!pg:%{p:mcrt0%O%s}%{!p:crt0%O%s}}}"
#endif
Listing 8-3: the default STARTFILE_SPEC
(gcc/gcc.cc:891-895). Note crt0, and note that
ENDFILE_SPEC defaults to the empty string at :897-900.
That default is a historical fallback almost nobody uses. Do not repeat the claim
that STARTFILE_SPEC supplies crt1.o crti.o crtbegin.o — that is one target
family's override. Here is the real one, for GNU userspace:
#define GNU_USER_TARGET_STARTFILE_SPEC \
"%{shared:; \
pg|p|profile:%{static-pie:grcrt1.o%s;:gcrt1.o%s}; \
static:crt1.o%s; \
static-pie:rcrt1.o%s; \
" PIE_SPEC ":Scrt1.o%s; \
:crt1.o%s} " \
GNU_USER_TARGET_CRTI " \
%{static:crtbeginT.o%s; \
shared|static-pie|" PIE_SPEC ":crtbeginS.o%s; \
:crtbegin.o%s} \
...
Listing 8-4: the glibc startfile spec
(gcc/config/gnu-user.h:51-65, installed over the default at
:66-67).
This is worth reading slowly, because it is the best example in the tree of the
if/else-if/else form from Chapter 1.4. Six arms select one crt1 variant:
| You passed | You get |
|---|---|
-shared | nothing at all (the empty first arm) |
-pg, -p or -profile | gcrt1.o, or grcrt1.o with -static-pie |
-static | crt1.o |
-static-pie | rcrt1.o |
whatever PIE_SPEC matches | Scrt1.o |
| otherwise | crt1.o |
Then a second, independent selection picks crtbeginT.o, crtbeginS.o or
crtbegin.o. Which is why gcc -static -### hello.c and
gcc -shared -### hello.c produce link lines that share almost no startup objects,
and why "the crt1 file" is not a well-defined phrase.
The crti.o and crtn.o in the middle come from two one-line macros:
#define GNU_USER_TARGET_CRTI "crti.o%s"
#define GNU_USER_TARGET_CRTN "crtn.o%s"
Listing 8-5: (gcc/config/gnu-user.h:43-44).
and the matching ENDFILE_SPEC mirrors the whole structure at
gnu-user.h:75-85.
Do not assume
crti.ocomes from the C library. On many bare-metal targets libgcc supplies it. Whichcrt*files a given target's libgcc provides is declared inlibgcc/config.hostasextra_parts— for example,arm*-*-eabi*getsextra_parts="crtbegin.o crtend.o crti.o crtn.o"(libgcc/config.host:584). On such a target, a missingcrti.omeans your prefix is wrong, not your sysroot, and the table above does not apply. Check what your libgcc installed before blaming anything.
Those extra_parts become EXTRA_PARTS in libgcc's makefile
(libgcc/Makefile.in:59-61) and get installed into
inst_libdir = $(libsubdir)$(MULTISUBDIR)
Listing 8-6: where libgcc's objects land
(libgcc/Makefile.in:315; the install rule is install-leaf at
:1188).
which is libsubdir plus the multilib subdirectory from Chapter 1.7. Note that
libgcc computes libsubdir from real_host_noncanonical
(libgcc/Makefile.in:204) rather than from a target variable —
because libgcc is configured with --host=<GCC's target>. Whenever a path formula
in a target library says "host", read "GCC's target". That convention will bite
you again in Chapter 1.12.
Why -lgcc is there twice
This is the one everybody eventually has to debug. The sequence is:
-lgcc -lc -lgcc
│ │ └── libgcc AGAIN, after libc
│ └──────── libc
└──────────────── libgcc
And it is one spec:
/* This is overridable by the target in case they need to specify the
-lgcc and -lc order specially, yet not require them to override all
of LINK_COMMAND_SPEC. */
#ifndef LINK_GCC_C_SEQUENCE_SPEC
#define LINK_GCC_C_SEQUENCE_SPEC "%G %{!nolibc:%L %G}"
#endif
Listing 8-7: the double libgcc
(gcc/gcc.cc:987-992). %G is the libgcc spec, %L the lib
spec.
Why: libc can call into libgcc — __divdi3 on a target without a division
instruction, the unwinder, soft-float helpers. With static archives the linker
resolves left to right and only pulls in the members it needs at the moment it
reads the archive. So a reference created by libc.a after libgcc.a had already
been scanned would go unresolved. Repeating libgcc after libc closes that.
-nolibc drops the middle pair and keeps the leading -lgcc, which you can read
directly off the spec: the %{!nolibc:…} wraps %L %G and not the first %G.
The lib spec on the other side of it has a default worth knowing:
/* config.h can define LIB_SPEC to override the default libraries. */
#ifndef LIB_SPEC
#define LIB_SPEC "%{!shared:%{g*:-lg} %{!p:%{!pg:-lc}}%{p:-lc_p}%{pg:-lc_p}}"
#endif
Listing 8-8: the default LIB_SPEC (gcc/gcc.cc:754-757).
So -lc is the default, -lc_p under profiling, and nothing at all when building
a shared object. Most targets override this in their config/*.h.
What libgcc actually is
GCC's own runtime support library. The compiler emits calls into it whether you asked for it or not:
- integer division and modulo helpers on targets without the instruction —
__divdi3,__udivsi3 - ARM EABI helpers — the
__aeabi_*family - soft-float emulation
- the stack unwinder used by C++ exceptions and by
__attribute__((cleanup))
This is why you still need
-lgccunder-nostdlib. Dropping the standard libraries does not stop the compiler emitting a call to__aeabi_idiv. A bare-metal link failing on an undefined__aeabi_*or__udivsi3is the single most common consequence, and Chapter 1.10 covers what you owe once you have taken the runtime away.
And note what %G expands to is not simply -lgcc. On a shared-libgcc target the
slot was rewritten at startup — Listing 4-5 — into the nested conditional that
chooses between -lgcc -lgcc_eh, -lgcc --as-needed -lgcc_s --no-as-needed and
-lgcc_s -lgcc depending on -static, -static-libgcc and -shared-libgcc.
Which is why:
$ gcc -### foo.o 2>&1 | tr ' ' '\n' | grep lgcc
$ gcc -### -static foo.o 2>&1 | tr ' ' '\n' | grep lgcc
give you different answers on the same toolchain.
Watching a flag take effect
The fastest way to internalise Listing 8-1 is to diff its output against itself:
$ gcc -### hello.c # baseline
$ gcc -### -nostdlib hello.c # crt*.o and -lc/-lgcc gone
$ gcc -### -nostartfiles hello.c # crt*.o gone, -lc/-lgcc kept
$ gcc -### -nodefaultlibs hello.c # crt*.o kept, -lc/-lgcc gone
$ gcc -dumpspecs | grep -A2 '^\*link_command'
Each of those four differences is one %{!…:} guard failing.
Things that surprise people
g++ reorders your -lm and -lc. They are hoisted out of wherever you put
them and re-emitted after -lstdc++. This is a real, silent reordering of your
command line, and on a target where everything is a static archive it changes
symbol resolution. Chapter 1.11.
-r behaves exactly like -nostdlib as far as these guards are concerned. A
relocatable link never gets the runtime — read the %{!r: in each of the three
guards.
-static-libstdc++ does not appear in the link line on most targets. It is
consumed by the C++ driver and turned into -Wl,-Bstatic … -Wl,-Bdynamic around
one -l. Grep for Bstatic, not for the option. Chapter 1.11.
ENDFILE_SPEC is empty by default (Listing 8-3). If crtend.o is not on your
link line, that may be entirely correct for your target.
A successful link says nothing about run time. Finding libstdc++.so.6 or
libc.so.6 when the program starts is the target loader's job — DT_RUNPATH,
ld.so.conf, LD_LIBRARY_PATH — and nothing in this chapter, or in this whole
part of the book, touches it. Link-time search and run-time search are unrelated
systems that happen to involve some of the same filenames.
Documentation coverage
-nostdlib and friends are documented under Link Options
(gcc/doc/invoke.texi:19067), and libgcc has its own chapter in the internals
manual.
What is not documented:
- Why
-lgccappears twice. The reason exists only as reasoning you have to reconstruct; the spec is there, the rationale is not. The nearest thing is the comment in Listing 8-7, which explains why the ordering is overridable rather than why it is what it is. - That
STARTFILE_SPEC's default is crt0-based and that the familiar crt1/crti/crtbegin shape is a per-target-family override. Every secondhand account of GCC's link order presents one target's spec as universal. - That
crti.omay come from libgcc rather than libc, which inverts the diagnosis table above on bare-metal targets.
Next: the same question for headers.
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.