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.

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:

  1. The startup files come from two unrelated providers, interleaved.
  2. 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

FilesProviderInstalled underMoved by
crt1.o, Scrt1.o, gcrt1.o, crti.o, crtn.othe C librarythe sysroot's /usr/lib--with-sysroot, --sysroot
crtbegin*.o, crtend*.oGCC'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 verbatimMeans
crt1.oyour sysroot is wrong
crtbegin.oyour 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 passedYou get
-sharednothing at all (the empty first arm)
-pg, -p or -profilegcrt1.o, or grcrt1.o with -static-pie
-staticcrt1.o
-static-piercrt1.o
whatever PIE_SPEC matchesScrt1.o
otherwisecrt1.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.o comes from the C library. On many bare-metal targets libgcc supplies it. Which crt* files a given target's libgcc provides is declared in libgcc/config.host as extra_parts — for example, arm*-*-eabi* gets extra_parts="crtbegin.o crtend.o crti.o crtn.o" (libgcc/config.host:584). On such a target, a missing crti.o means 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 -lgcc under -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 __udivsi3 is 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 -lgcc appears 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.o may 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.