Main

Blog (Atom feed)

GCC specs for those deeply interested

In the previous article on GCC specs I covered the basics. In this article I'm going to get into advanced usage and clarify some statements from the previous one that were (intentionally) misleading.

There will be links to the driver source here as it is helps in understanding what is going on. It will be helpful to at least glance at it. It is also a good idea to review the parts about experimenting with specs in the previous article because I'm not going to demonstrate as many behaviours and you may want to try things as you go.

For convenience, here's a link to the official documentation on specs. It's good to have that handy.

Where the driver finds specs

To understand how specs are found you have to look at the driver source code. Specifically the method driver::set_up_specs.

Near the beginning of the method you'll see that it looks for a file called specs in the startfile prefixes. You can find the startfile prefixes by running gcc -print-search-dirs and looking at the "libraries" entry. See the code in driver::maybe_print_and_exit for a reference.

The output of -print-search-dirs can be a hard to read (this is a theme of GCC output) so you can use the following to simplify it, preserving the search order.

gcc -print-search-dirs \
  | sed -n -e '/^libraries: =/ {s/.*: =//; s/:/\n/g; p}' \
  | xargs realpath -q \
  | awk '{if (!seen[$0]) print; seen[$0] = 1;}'

Once you have this list you might think that you can put a file named specs in one of the directories and it will override the built-in specs.

Is it really that simple? Of course not.

First of all, the list of startfile prefixes at the time this spec file is read is not the same as the one printed via -print-search-dirs. The directory that probably will work is going to be the same one that shows up in the "install" entry. You'll know you've found it when the initial output from -v or -### starts with "Reading specs from" instead of "Using built-in specs".

And more importantly, this file won't actually override the built-in specs!

When a spec file is read the driver eventually calls set_spec. In that function if the global array of specs is not initialized it will initialize it to the set of static built-in ones. So you can't actually override the built-in specs unless you override each entry.

This is a lot of work to do manually, so if you really want to change them all it's best to save the built-in ones and alter them.

You can always pass specs using the --specs option (these are "user specs"), but you may want to use the specially placed spec files if you have some site-wide change you want to make, or you need to set specs early and affect spec processing that happens before user specs are processed.

Any argument to the --specs option will be searched for in the library directories if the name of the file is not an absolute path. This means you can ship spec files with the compiler and use them by name instead of using full pathnames. If you have the Arm GCC cross compiler installed you probably use this feature when linking bare-metal applications. In that case you are likely passing --specs=nosys.specs to link with a dummy syscall library. So this mechanism does get used in actual compiler distributions.

It's also worth noting that you can't use -dumpspecs to see the specs that you've set. When dumping specs the driver always uses the built-in ones and exits before any spec files are read. If you want to view the specs that get set by any files you provide you have to build the driver with -DDEBUG_SPECS added to CXXFLAGS and use -v when running.

Augmenting specs

Wholesale replacement of the built-in specs is generally not a good idea. What you'll likely want to do is alter an existing spec by adding to the beginning or the end. There are standard ways to do this.

To add to the beginning you have to rename a spec. There is an example in the documentation. Here is another that will add --special-option before any other assembler arguments in the asm spec.

%rename asm old_asm

*asm:
--special-option %(old_asm)

To add to the end of a spec, use the plus sign (+) followed by a space. The documentation commits a sin of omission here because it says that the spec only has to start with the + symbol for it to be appended.

The example below would enable stack protector for every C file you compile.

*cc1:
+ -fstack-protector

Altering the full contents of a spec string without copying it to a file and modifying it requires messing around with spec functions and changing the source code. It's not something you'll want to do unless you have some really specific requirements. (And if you think you really need to do this, I recommend you keep thinking.)

Enterprising readers may have noticed that you can override the specs for a suffix, which means you can change the spec used for file types. If you want to do this effectively you need to study the default file type specs used by the driver. Those are found in the array default_compilers.

To get your imagination going, here's how you can change the assembler to run a different assembler. The conditionals that would prevent the assembler from running have been omitted for clarity.

.s:
custom-as %(asm_debug) %(asm_options) %i %A 

If you do this you may notice that compiling a .c file does not result in your custom assembler being used. This is because spec processing doesn't actually go in "compiler-assembler-linker" order; it goes in "compiler-linker" order. It is up to the spec for the language to invoke a series of commands to create an object file for the linker. This is what the driver considers to be a "compiler". The assembler spec we have defined above is the compiler for files given to the driver that end with .s.

If you look at the spec for the .c files in the driver source it will be associated with the @c entry which is the actual spec used. Reading it will eventually reveal it uses the invoke_asm spec to call the assembler. Doing a full override of specs is tough: there are many cases to consider!

(Side note: you can override @ entries. They are considered to be special suffixes. The above example could be written with @assembler: as the first line. In this case it would work for any file suffix that is aliased to the assembler.)

Altering the driver's arguments

There is an interesting entry in the -dumpspecs output that will almost certainly be empty in your installation: self_spec.

What is interesting about the self spec is that it is applied to the arguments for the driver. This means you can change the driver arguments from within the driver itself.

The driver has built-in self specs that can only be altered by rebuilding from source. They are defined by the target backend. However, you can define user self specs which are applied after all other specs have been read.

Use of self specs tend to be target-specific or needed for highly customized situations. Anything added via a self spec has to be an argument that starts with a hyphen (-).

For an illustrative—but not really motivational—example, suppose we changed the driver options such that all instances of -m32 are removed and we always add -m64 to the arguments. It can be done this way.

*self_spec:
%<m32 -m64

Changing the driver arguments affects all subprocesses and must be done with care. You can cause a lot of trouble with this; it's much like eval in dynamic languages.

When the driver runs a subprocess the environment variable COLLECT_GCC_OPTIONS contains all the driver arguments after all the self specs have been processed. When running with verbose output use it to debug any use of self specs.

Subtleties around spaces

In the previous article I said this:

All directives, once substituted, have spaces around them so you can't piece together multiple arguments into one argument.

That's not entirely true. You can concatenate multiple arguments into a single one, but it will not always work as expected. (There were multiple counterexamples in the previous article, if you looked closely.)

The official documentation says this about directives.

Note that spaces are not generated automatically around the results of expanding these sequences. Therefore you can concatenate them together or combine them with constant text in a single argument.

Let's test that out using the following spec file, assuming I have a file test.sh in the current directory that only prints its arguments.

.xx:
./test.sh /%{O1:1/opt}/%{g3:3/debug}

Trying this out, I get

% gcc --specs=local.specs -c t.xx -O1 -g3
/1/opt/3/debug

Great! But this is specific to the -O and -g levels. Let's say I want to generalize it and base if off the values passed to those options.

.xx:
./test.sh /%{O*:%*/opt}/%{g*:%*/debug}

Now I get something different.

% gcc --specs=local.specs -c t.xx -O1 -g3
/1/opt /3/debug

The %* substitution will cause a space to be added because of the code in give_switch.

While it is true that directives are substituted without adding a space (notwithstanding the example above), a spec could have leading or trailing spaces. Consider the following example.

# There is a space after the word "middle"
*stuff:
%{dummy} middle 

.xx:
./test.sh BEGIN%(stuff)END
% gcc --specs=local.specs -c t.xx
BEGIN middle END

In this case %{dummy} expands to the empty string which is followed by a space, and the spec for stuff ends in a space. (Note that leading spaces in a spec are skipped when it is read but not when it is processed.) Sure, the directives are concatenated. But that does not mean that putting two specs directly beside each other will result in a contiguous string with no space separating them. This means if you are trying to create pathnames using substitutions, for example, it can be tricky.

Spec functions

Spec functions are a generic mechanism for providing specs. They must be defined in source code. They deserve their own article so I will not be covering them much here.

Spec functions can be used in two ways: for direct substitution or conditional substitution. Most spec functions only make sense in one of those contexts. In a conditional context, if a spec function returns NULL it is considered false. If it returns any string (even empty) it is considered true.

Target backends may define their own spec functions but these will not be in the official documentation. You have to go into the source to find them.

Here's an example that adds the -g3 argument for cc1 if the last -O argument is -O2 or higher.

*cc1:
%{%:gt(%{O*:%*;:0} 1):-g3}

The semantics of spec functions are where specs get really complicated and bizarre. Stay tuned for another article that will delve into that mess.

January 9, 2024

comment@wozniak.ca

Generated on 2024-01-09