Specs: how the driver writes a command line

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

You pass -static-libgcc and the link line changes. You go looking for the code that implements it and there isn't any. There is no if (static_libgcc) anywhere in the link path, no function that consults a flag and appends a library.

What there is instead is a string:

%{static|static-libgcc|static-pie:-lgcc -lgcc_eh}%{!static:%{!static-libgcc: …

GCC's driver builds every command line by expanding templates written in a small %-escape language. Roughly: printf for command lines, with conditionals and function calls bolted on. This chapter is about reading and writing that language, because once you can, you can change a toolchain's link behaviour without rebuilding it — and you can explain why several otherwise baffling options behave the way they do.

The order here is deliberate. The names come first, then the % directives that reference them. The shorthands are meaningless until you know what they point at.

The table

A spec is a named entry in a table the driver holds in memory: a name, and the template text currently bound to it. -dumpspecs prints the whole table, and every line starting with * is a name:

$ gcc -dumpspecs | grep '^\*'                    # every name your driver has
$ gcc -dumpspecs | sed -n '/^\*libgcc:/,/^$/p'   # one name and its value

The table is a linked list of a struct whose first two comment lines tell you something important:

struct spec_list
{
				/* The following 2 fields must be first */
				/* to allow EXTRA_SPECS to be initialized */
  const char *name;		/* name of the spec.  */
  const char *ptr;		/* available ptr if no static pointer */
  ...
  const char **ptr_spec;	/* pointer to the spec itself.  */

Listing 4-1: struct spec_list (gcc/gcc.cc:1685-1700).

Note ptr_spec: an entry does not hold the text, it holds a pointer to the C variable that holds the text. The table is seeded from a static array:

  INIT_STATIC_SPEC ("libgcc",			&libgcc_spec),
  INIT_STATIC_SPEC ("startfile",		&startfile_spec),

Listing 4-2: two entries from static_specs[] (gcc/gcc.cc:1729-1730; the array begins at :1707).

Everything that mutates a spec — a target's override, a -specs= file, the driver's own startup rewriting — goes through one function and writes through that pointer (set_spec, gcc/gcc.cc:2066). Nothing else caches the text. That indirection is the single most useful fact in this chapter, and the reason is coming in a moment.

The names you will actually meet:

NameHolds
cppoptions for the preprocessor
cc1 / cc1plusoptions for the C / C++ compiler proper
asmoptions for the assembler
linkoptions for the linker
libthe default system libraries — usually -lc
libgccGCC's helper library — -lgcc and friends
startfileobjects linked before yours
endfileobjects linked after yours
link_commandthe complete template for the whole link

That set is hardcoded into the driver executable. There is no external file or database declaring which names exist; it is fixed when GCC is built, from three sources:

SourceContributes
static_specs[] in gcc/gcc.cc:1707the universal names above, on every target
target OS headers, e.g. gcc/config/gnu-user.h#define STARTFILE_SPEC …replacements for the defaults
target CPU headers, e.g. gcc/config/aarch64/aarch64.h#define EXTRA_SPECS …additional names such as asm_cpu_spec

This is why two GCCs built from identical source for different targets behave completely differently. Same code, different #defines. And it is why -dumpspecs output is target-specific: an aarch64-none-elf-gcc and an x86_64-linux-gnu-gcc list different names. Always dump your compiler rather than trusting a transcription, including the ones in this book.

There are two ways to use a name. You can reference it from inside another spec — %(link) expands the link spec right there, and the hot-path names have one-letter aliases so %l means the same thing. Or you can override it from a specs file. Both matter later.

Reading a conditional

Take the libgcc spec on a target with a shared libgcc. Its value is roughly:

%{static|static-libgcc|static-pie:-lgcc -lgcc_eh}
%{!static:%{!static-libgcc:%{!static-pie:
  %{!shared-libgcc:-lgcc --as-needed -lgcc_s --no-as-needed}
  %{shared-libgcc:-lgcc_s%{!shared: -lgcc}}}}}

There are only two constructs in there. %{FLAG:text} emits text if the flag was given; %{!FLAG:text} emits it if the flag was not. Everything else is nesting, and three details make the nesting readable:

  • | is or. The first line fires if any of -static, -static-libgcc or -static-pie was given.
  • There is no &&. Nesting negations is how you say and-not: %{!a:%{!b:X}}.
  • The output half can itself contain conditionals. X in %{S:X} is more spec text, expanded recursively — which is why %{shared-libgcc:-lgcc_s%{!shared: -lgcc}} appends -lgcc only when you are not building a shared object.

Work it out and you get:

Your commandlibgcc expands to
gcc foo.o-lgcc --as-needed -lgcc_s --no-as-needed
gcc -static foo.o-lgcc -lgcc_eh
gcc -shared-libgcc foo.o-lgcc_s -lgcc
gcc -shared -shared-libgcc foo.o-lgcc_s

Watch it happen:

$ gcc -### -static-libgcc foo.o 2>&1 | tr ' ' '\n' | grep lgcc

So -static-libgcc is not implemented by any C code in the link path. It is a switch tested by a conditional inside one string, which is exactly why replacing the string changes the behaviour with no rebuild.

Why %G names a slot, not a value

Here is where the pointer indirection from Listing 4-1 earns its keep, and where most secondhand explanations of GCC go wrong.

You will see the arrow %G → LIBGCC_SPEC written as though it were an equality. It is a provenance note. Four distinct things share that name:

#ThingKindWhere
1LIBGCC_SPECa C preprocessor macro, overridable by the targetgcc.cc:878-889
2libgcc_speca C variable, initialised from the macrogcc.cc:1217
3"libgcc"the name that variable is registered undergcc.cc:1729
4%Ga directive meaning "expand whatever is registered as libgcc"gcc.cc:6813

The implementation of the directive is two lines and settles it:

	  case 'G':
	    value = do_spec_1 (libgcc_spec, 0, NULL);

Listing 4-3: %G (gcc/gcc.cc:6813-6814). do_spec_1 is the expander.

So %G does not stand for the text of LIBGCC_SPEC. It recursively expands the current value of the libgcc spec, which merely started life as that macro. It is exactly equivalent to %(libgcc), whose handler resolves the name through the same table (gcc.cc:6931).

That distinction is not pedantry, because the value gets rewritten at startup. The macro's own text is:

#define LIBGCC_SPEC "-lgcc"

Listing 4-4: the default LIBGCC_SPEC (gcc/gcc.cc:887).

Three words. But on a target configured with ENABLE_SHARED_LIBGCC, the driver walks that string at startup looking for the literal -lgcc and replaces it wholesale:

    const char *p = libgcc_spec;
    int in_sep = 1;

    /* Transform the extant libgcc_spec into one that uses the shared libgcc
       when given the proper command line arguments.  */
    while (*p)
      {
	if (in_sep && *p == '-' && startswith (p, "-lgcc"))
	  {
	    init_gcc_specs (&obstack,
			    "-lgcc_s"

Listing 4-5: the shared-libgcc rewrite (gcc/gcc.cc:1922-1931).

The replacement text is assembled by init_gcc_specs (gcc/gcc.cc:1816-1851) and depends on USE_LD_AS_NEEDED and LINK_EH_SPEC, so it differs between targets. That is why the conditional in the previous section is "roughly" and not exactly, and why you should run -dumpspecs on your own toolchain.

The string a beginner reads in the driver source is not the string that expands.

Position is the point

The second worked example is the whole link, in one template. Trimmed to what matters:

%{!fsyntax-only:%{!c:%{!M:%{!MM:%{!E:%{!S:      ← only when actually linking
    %(linker)  %l                               ← collect2/ld, then the link spec
    %X %{o*} %{e*}                              ← your -Wl, options; -o; -e
    %{!nostdlib:%{!r:%{!nostartfiles:%S}}}      ← startfile: crt1 crti crtbegin
    %o                                          ← YOUR object files
    %{!nostdlib:%{!r:%{!nodefaultlibs:
        %(link_gcc_c_sequence)}}}               ← expands to  %G %L %G
    %{!nostdlib:%{!r:%{!nostartfiles:%E}}}      ← endfile: crtend crtn
    %{T*} }}}}}}

Listing 4-6: LINK_COMMAND_SPEC, abridged (gcc/gcc.cc:1159-1178).

%l %S %E %G %L are one-letter aliases for the names link, startfile, endfile, libgcc and lib. So Listing 4-6 contains the previous section by reference: each %G expands that entire nested conditional.

Three things to take from it.

Position in the template is position on the command line. %S%o … libraries … %E is the link order. The whole startfile/endfile split exists because %S and %E sit on opposite sides of your object files. That is the subject of Chapter 1.8.

-nostdlib, -nostartfiles, -nodefaultlibs and -r are pure conditionals. No C code implements them; they fail a %{!…:} guard and the text disappears. The guards are on three specific lines — gcc.cc:1168, :1176 and :1177 — and reading the nesting gives you the truth table directly. Chapter 1.10 does that.

A target customises one small name, not this whole string. The %G %L %G ordering lives in its own spec precisely so a port can change it, and the comment above it says so:

/* 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 4-7: LINK_GCC_C_SEQUENCE_SPEC (gcc/gcc.cc:987-992).

Yes, %G really is there twice. Chapter 1.8 explains why.

Directives that expand state, not text

The third family reads the driver's own state or names files. You cannot see their values by looking at a spec — but every one of them is printable with a command from Chapter 1.3.

DirectiveReadsEmitsPrint it with
%Dthe library search listone -L per directory-print-search-dirs
%Rthe sysrootthe bare path-print-sysroot
%Mthe multilib OS directory., ../lib32, ../lib64-print-multi-os-directory
%IGCC's own header directories-isystem …/include, …/include-fixed-print-file-name=include
%X %Y %Zyour -Wl, / -Wa, / -Wp, optionsthem, prefix stripped-###

The driver's own documentation of %D and %R, from the long comment that defines the language:

 %D	Dump out a -L option for each directory in startfile_prefixes.
	If multilib_dir is set, extra entries are generated with it affixed.
 ...
 %R	Output the concatenation of target_system_root and
        target_sysroot_suffix.

Listing 4-8: two directives, self-documented (gcc/gcc.cc:592-593 and :599-600).

%R is the sysroot mechanism. When Chapter 1.2 said a sysroot is "a string glued onto the front of certain paths", %R is where the gluing is written for spec-level consumers. A target that wants sysroot-relative link behaviour puts %R in its spec; one that does not, does not. Its implementation is exactly the concatenation the comment describes (gcc.cc:6819-6830).

Then the file-naming family:

DirectiveNames
%ithe input file for this step
%oall output files — in the link, where your .o files land
%Othe object suffix, .o
%slook this name up in the library search list
%g… %u… %U… %j…temporary files, differing in whether the name is shared or unique

You met %g in Chapter 1.1 without being told its name. The scratch .s file that carries assembly from cc1 to as is %g.s expanding — shared, so two separate programs agree on a file nobody named:

$ gcc -### hello.c 2>&1 | grep -o '/tmp/cc[A-Za-z0-9]*\.s'

%s, which causes the most confusion of any directive

Consider a startfile spec containing:

crt1.o%s   crti.o%s   crtbegin.o%s
└─ from libc ─────┘   └─ from libgcc ─┘

All three are written identically. Yet crt1.o comes from the C library's sysroot and crtbegin.o from GCC's own prefix. There is no hint of that asymmetry in the spec, because %s means nothing more than "search for this name", and the driver's comment is explicit that it is one list:

 %s     current argument is the name of a library or startup file of some sort.
        Search for that file in a standard list of directories
	and substitute the full name found.

Listing 4-9: %s (gcc/gcc.cc:571-573).

One search list; two providers; the difference invisible in the spec and visible in one command:

$ gcc -print-file-name=crt1.o        # the libc half
$ gcc -print-file-name=crtbegin.o    # the libgcc half

The rest of the language

The language documents itself in a 230-line comment at gcc/gcc.cc:471-700, which is worth reading once end to end. The summary:

Name references. %(name) for any name; %G, %L, %S, %E, %l, %a, %1, %2, %C as one-letter aliases for libgcc, lib, startfile, endfile, link, asm, cc1, cc1plus, cpp.

Conditionals, the %{...} family:

FormMeaning
%{S}emit -S if it was given
%{S*}emit every switch starting -S, arguments included
%{S:X}emit X if -S was given
%{!S:X}emit X if -S was not given
%{S|T:X}emit X if -S or -T
%{.S:X}emit X if the input file has suffix .S
%{S:X;T:Y;:D}if / else-if / else, with D as the default arm

That last form is worth knowing because real target specs use it heavily — %{msoft-float:-soft;:-hard} emits one or the other, and Chapter 1.8 shows a glibc startfile spec that selects among five different crt1 variants with it. Conditionals are dispatched to one function ([case '{' at gcc.cc:6840, handle_braces at :7267).

Escaping is a backslash, which is why you will see %{std=iso9899\:1999:X} — without it, that colon would be read as the :X separator.

Function calls, %:name(args), dispatched through a registry:

static const struct spec_function static_spec_functions[] =
{
  { "getenv",                   getenv_spec_function },
  { "if-exists",		if_exists_spec_function },
  ...

Listing 4-10: the spec-function table (gcc/gcc.cc:1775-1801), dispatched at case ':', :6846.

The useful ones are %:getenv(VAR SUFFIX), %:if-exists(f) and %:if-exists-else(a b) for picking a file that is actually present, %:include(libgomp.spec) for pulling in another specs file mid-expansion, %:version-compare(...) for Darwin-style OS gating, %:sanitize(address) to test which sanitizer is on, and the numeric predicates %:gt, %:debug-level-gt, %:dwarf-version-gt. The return string is re-processed as spec text. Used as a predicate, %{%:function(args):X} emits X when the function returns something non-empty.

Note that the table ends with EXTRA_SPEC_FUNCTIONS, so a port can add its own.

Writing a specs file

A specs file overrides entries in the table. This is how you change a toolchain's behaviour without rebuilding it, and it is the mechanism behind, for example, ARM's nano.specs.

*libgcc:
-lgcc -lgcc_eh

*startfile:
+ my-extra-crt.o

Listing 4-11: a two-entry specs file.

The syntax is: *name: alone on a line, the value on the following lines, terminated by a blank line. A value beginning with + — plus, then a space — appends to the existing value instead of replacing it, and that is one line of C:

  *(sl->ptr_spec) = ((spec[0] == '+' && ISSPACE ((unsigned char)spec[1]))
		     ? concat (old_spec, spec + 1, NULL)
		     : xstrdup (spec));

Listing 4-12: replace or append, in set_spec (gcc/gcc.cc:2106-2108).

Two directives work only in a file, not in a spec: %rename old new moves a spec's text to a new name (gcc.cc:2454), and %include FILE / %include_noerr FILE splice in another file (:2411).

%rename exists for the idiom you will actually want, which is adding to a spec rather than replacing it. Rename the original out of the way, then reference it:

%rename link old_link

*link:
%(old_link) -extra-flag

Listing 4-13: the wrapping idiom.

A specs file may also introduce an entirely new name — set_spec creates the entry if it does not exist (gcc.cc:2066) — which is only useful if something references it with %(name). That is exactly what Listing 4-13 is doing.

Then:

$ gcc -specs=my.specs -### hello.c    # see exactly what your override did

Multiple -specs= are applied in the order given.

Where specs come from, in order

Later entries win:

  1. Built-in defaults, compiled into the driver, each overridable by a target.
  2. Target overrides, from your target's config headers.
  3. Driver startup rewrites — the libgcc rewrite of Listing 4-5 is the one you are most likely to meet, and it is what makes -static-libgcc work at all.
  4. A specs file next to the driver (gcc.cc:8496-8499). Not shipped by default, but if one is present it silently rewrites the toolchain.
  5. -specs=FILE on the command line (gcc.cc:8633-8640).

Debugging

$ gcc -dumpspecs                       # the whole table, current values
$ gcc -### hello.c                     # every expanded command line, nothing run
$ gcc -specs=my.specs -### hello.c     # what your override actually did

For the truly stuck, the driver has a compile-time DEBUG_SPECS that narrates every %(name) expansion and every set_spec (gcc.cc:2110-2113). It is #ifdef-only: you have to rebuild the driver with it defined, which puts it firmly in the last-resort category.

Documentation coverage

The spec language has its own manual node, Spec Files (gcc/doc/invoke.texi:37290), and it is a decent reference for the directives. What it does not cover, and what this chapter exists for:

  • That %G expands a mutable slot rather than the macro's text, and that the slot is rewritten at startup on shared-libgcc targets. The manual describes %G as processing LIBGCC_SPEC, which is true only before startup.
  • That an installed specs file overrides the built-in table.
  • That --with-specs rewrites your command line via driver_self_specs rather than editing the table, and therefore cannot appear in -dumpspecs.

Next: the search lists that %D, %I, %R and %s all draw on.


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.