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:
| Name | Holds |
|---|---|
cpp | options for the preprocessor |
cc1 / cc1plus | options for the C / C++ compiler proper |
asm | options for the assembler |
link | options for the linker |
lib | the default system libraries — usually -lc |
libgcc | GCC's helper library — -lgcc and friends |
startfile | objects linked before yours |
endfile | objects linked after yours |
link_command | the 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:
| Source | Contributes |
|---|---|
static_specs[] in gcc/gcc.cc:1707 | the 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-libgccor-static-piewas given.- There is no
&&. Nesting negations is how you say and-not:%{!a:%{!b:X}}. - The output half can itself contain conditionals.
Xin%{S:X}is more spec text, expanded recursively — which is why%{shared-libgcc:-lgcc_s%{!shared: -lgcc}}appends-lgcconly when you are not building a shared object.
Work it out and you get:
| Your command | libgcc 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:
| # | Thing | Kind | Where |
|---|---|---|---|
| 1 | LIBGCC_SPEC | a C preprocessor macro, overridable by the target | gcc.cc:878-889 |
| 2 | libgcc_spec | a C variable, initialised from the macro | gcc.cc:1217 |
| 3 | "libgcc" | the name that variable is registered under | gcc.cc:1729 |
| 4 | %G | a 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.
| Directive | Reads | Emits | Print it with |
|---|---|---|---|
%D | the library search list | one -L per directory | -print-search-dirs |
%R | the sysroot | the bare path | -print-sysroot |
%M | the multilib OS directory | ., ../lib32, ../lib64 | -print-multi-os-directory |
%I | GCC's own header directories | -isystem …/include, …/include-fixed | -print-file-name=include |
%X %Y %Z | your -Wl, / -Wa, / -Wp, options | them, 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:
| Directive | Names |
|---|---|
%i | the input file for this step |
%o | all output files — in the link, where your .o files land |
%O | the object suffix, .o |
%s | look 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:
| Form | Meaning |
|---|---|
%{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:
- Built-in defaults, compiled into the driver, each overridable by a target.
- Target overrides, from your target's config headers.
- Driver startup rewrites — the
libgccrewrite of Listing 4-5 is the one you are most likely to meet, and it is what makes-static-libgccwork at all. - A
specsfile next to the driver (gcc.cc:8496-8499). Not shipped by default, but if one is present it silently rewrites the toolchain. -specs=FILEon 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
%Gexpands a mutable slot rather than the macro's text, and that the slot is rewritten at startup on shared-libgcc targets. The manual describes%Gas processingLIBGCC_SPEC, which is true only before startup. - That an installed
specsfile overrides the built-in table. - That
--with-specsrewrites your command line viadriver_self_specsrather 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.