Demystifying Arm GNU Toolchain Specs: nano and nosys

September 19, 2023

Introduction

What is the purpose of nano.specs and nosys.specs ? What is a GCC Spec ? In this post, I will give an answer to these questions.

I have actually written this as part of another post, but this is quite an independent topic and its audience might be much larger, so I decided to make it a standalone post.

I am using an Arm Cortex-M33 processor, specifically an STM32H563 MCU, but this is not very important.

For this post, I am using STM32CubeIDE v1.13.1 which includes GNU Tools for STM32 (11.3.rel1) (I think it means Arm GNU Toolchain v11.3.Rel1), but also the latest Arm GNU Toolchain v12.3.Rel1 standalone.

I will start by showing what options STM32CubeIDE uses to build a program. Then, I will explain nano spec, newlib-nano, nosys spec and libnosys.

Arm GNU Toolchain is not only for Cortex-M, but I am biased towards Cortex-M, since this is the only platform I write code for and only platform I have access to. For newlib-nano and nosys, there does not seem to be any difference but I may miss something specific for Cortex-A and Cortex-R platforms.

STM32CubeIDE Build Settings

Because I am using STM32H563 and sometimes STM32CubeIDE, I first want to see how it builds a project. I have created an STM32 project by selecting:

  • NUCLEO-H563ZI board
  • Targeted Language: C
  • Targeted Device Usage: TrustZone not enabled
  • Targeted Binary Type: Executable
  • Targeted Project Type: Empty

Then, I have selected the Release configuration, Floating-point unit as None and Floating-point ABI as Software implementation in the project build settings.

STM32CubeIDE is not using GNU assembler as and GNU linker ld directly but uses the GNU Compiler Collection gcc to compile, to assembly and to link. gcc sounds like a C compiler but it is more than that. It is so called a driver program and runs other programs to do the job. What gcc actually does is based on the command-line options. You can use it as a compiler, as an assembler or as a linker. This also makes it possibly to use specs also for linking, since ld (and as) does not support specs.

For this project, STM32CubeIDE shows the following Compiler, Assembler and Linker options:

  • Compiler: -mcpu=cortex-m33 -std=gnu11 -DSTM32H563ZITx -DSTM32 -DSTM32H5 -DNUCLEO_H563ZI -c -I../Inc -Os -ffunction-sections -fdata-sections -Wall -fstack-usage -fcyclomatic-complexity --specs=nano.specs -mfloat-abi=soft -mthumb

  • Assembler: -mcpu=cortex-m33 -c -x assembler-with-cpp --specs=nano.specs -mfloat-abi=soft -mthumb

  • Linker: -mcpu=cortex-m33 -T"STM32H563ZITX_FLASH.ld" --specs=nosys.specs -Wl,-Map="${BuildArtifactFileBaseName}.map" -Wl,--gc-sections -static --specs=nano.specs -mfloat-abi=soft -mthumb -Wl,--start-group -lc -lm -Wl,--end-group

The common options in all three are:

  • -mcpu=cortex-m33: sets the target processor
  • -mfloat-abi=soft: floating point is not used or initialized in this project, so a software floating-point support is selected
  • -mthumb: thumb instruction set, actually it means Thumb-2 because the processor supports Thumb-2
  • --specs=nano.specs: uses newlib-nano, links with libc_nano.a

Omitting the debug like options such as -fstack-usage and -fcyclomatic-complexity, warnings like -Wall and device specific definitions like -DSTM32, the ones below are left in each category:

Compiler options:

  • -std=gnu11: selects C11 standard with GNU extensions
  • -ffunction-sections: places each function into its own section
  • -fdata-sections: places each data into its own section

Assembler options:

  • -x assembler-with-cpp: assembly files may contain C processor directives, so a preprocessor runs first. This is default if file extension is .S rather than .s.

Linker options:

  • -T"...": use the specified link script rather than using the default
  • -Wl,--gc-sections: unused code is eliminated, this requires objects to be compiled with -ffunction-sections and -fdata-sections
  • -static: does not link against shared libraries
  • --specs=nosys.specs: links with libnosys.a

The options most different than using C on a desktop are the nano and nosys specs.

Arm GNU Toolchain

Arm GNU Toolchain (12.3.Rel1) contains a few projects and as listed in its release notes, these projects are: GCC, glibc, newlib (which includes newlib-nano), binutils, GDB, libexpat, Linux Kernel, libgmp, libisl, libmpfr, libmpc and libiconv. For this post, GCC, newlib and binutils are very relevant. The assembler as, the linker ld and the tools like objdump are part of binutils. newlib provides not only newlib and newlib-nano but also libnosys, and also nano.specs and nosys.specs files. So, everything related to nano and nosys comes from newlib project.

In Arm GNU Toolchain (12.3.Rel1), the specs are under arm-none-eabi/lib folder:

$ ls -1 *.specs

aprofile-validation.specs
aprofile-validation-v2m.specs
aprofile-ve.specs
aprofile-ve-v2m.specs
iq80310.specs
linux.specs
nano.specs
nosys.specs
pid.specs
rdimon.specs
rdimon-v2m.specs
rdpmon.specs
redboot.specs

There are actually less “concepts” here, a few of specs belong to the same group.

  • aprofile-*.specs (all four): used for semihosting with librdimon*.a. It says these are for AArch32 VALIDATION and VE platforms, I do not know yet what these platforms mean. If you do not know what semihosting is, this will be a topic of another post.
  • linux.specs: for compiling a program to run on Linux on Arm, used with libgloss-linux.a.
  • nano.specs: for using _nano standard C libraries
  • nosys.specs: for compiling to bare-metal, used with libnosys.a.
  • rdimon*.specs (both): used for semihosting with librdimon*.a
  • rdpmon.specs: used for semihosting with librdpmon.a.
  • redboot.specs, iq80310.specs and pid.specs: used for redboot, these specs are actually same but each with a different memory address used when linking.

One difference between nano.specs and all others are, nano.specs also changes the compile options (in addition to link) whereas all others modify only the link options. Another difference is that nano.specs changes the standard C library, whereas all others are related to the interaction of the standard C library with the system. I did not test this but I think it can be said you have an option to use nano (newlib-nano) or not (thus newlib), and also you have an option to choose one of the other specs (e.g. nosys) or not (then it is assumed you will have syscalls in place in your system somehow).

All the standard libraries or libraries required to use some of these specs are also in the same folder:

$ ls -1 *.a

libc.a
libc_nano.a
libg.a
libgfortran.a
libgloss-linux.a
libg_nano.a
libm.a
libnosys.a
librdimon.a
librdimon_nano.a
librdimon-v2m.a
librdpmon.a
libstdc++.a
libstdc++_nano.a
libsupc++.a
libsupc++_nano.a

The meaning of these libraries are:

  • c: standard C library
  • g: standard C library with debug enabled
  • gfortran: Fortran shared library
  • gloss-linux: library for using Linux syscalls
  • m: math library. Some math functions of standard C are in this library. If a standard C function is not in the math library, then it is in the standard C library.
  • nosys: no system library for bare-metal applications
  • rdimon: remote debug interface monitor
  • rdpmon: remote debug protocol monitor
  • stdc++: standard C++ library
  • supc++: support library for C++ (for RTTI and exception handling)

There are two precompiled standard C libraries in Arm GNU Toolchain: newlib (libc.a, libg.a) and newlib-nano (libc_nano.a, libg_nano.a).

When C language is used, the programs are linked with the standard C library which is available in many platforms (such as glibc or newlib). In an embedded platform, naturally the resources and capabilities are limited, so it makes sense to use a minimal library and newlib-nano is one of them. Moreover, the standard C library depends on the system calls particularly for I/O. These calls are normally implemented by the operating system (you might only need a bridge or not depending on actual libraries, libgloss-linux.a is such a bridge). In a bare-metal application, there is no operating system, so some or most of the system calls are not available. In such a case, libnosys is used, because it implements the system calls just as stubs and returns errors.

It might be useful to know that the Arm GNU Toolchain is built with:

  • threads and thread local storage (tls) disabled (–disable-threads, –disable-tls)
  • native language support (nls) disabled (–disable-nls)
  • shared libraries disabled (–disable-shared)
  • newlib is target C library (–with-newlib)
  • assembler is GNU as (–with-gnu-as)
  • linker is GNU ld (–with-gnu-ld)
  • with multilib support for Cortex-A, Cortex-R and Cortex-M profiles

and binutils is built with:

  • with init and fini array support (–enable-initfini-array)
  • native language support (nls) disabled (–disable-nls)
  • –without-x (sounds like without X but not sure)
  • without tcl and tk (–disable-tcl, –disable-tk)
  • without gdb and disables gdb (–without-gdb, –disable-gdb, –disable-gdbtk)
  • enables plugins (–enable-plugins)

How Arm GNU Toolchain is built can be seen in the Linaro ABE manifest files in ARM GNU Toolchain download page. I will come back to this later for newlib and newlib-nano.

specs

specs provides a way to add, remove or modify the command-line options of gcc. My understanding is that gcc always runs with specs, and there are a few built-in specs. The built-in specs can be displayed with -dumpspecs option.

The complete documentation of specs and spec file syntax can be found in GCC command options: Specifying Subprocesses and the Switches to Pass to Them. The relevant built-in specs as documented in the link above are:

  • link: Options to pass to the linker
  • lib: Libraries to include on the command line to the linker

it is not mentioned in the documentation, but by looking to the source code of gcc, I think the meaning of the following specs are:

  • cpp_unique_options: the options used when processing C files
  • link_gcc_c_sequence: used for passing gcc and C libraries to linker

Although the spec file syntax is a bit strange, nano.specs and nosys.specs are not very complicated and not difficult to understand keeping the following rules in mind:

  • %rename old new renames the old spec to new
  • *spec adds, modifies or removes the spec depending on the following lines. If the result of following lines are empty, then the spec is removed.
  • %{S:X} means, if -S is given to GCC, it is replaced with X. Pay attention the first has no -.
  • %(spec) means to include whatever the spec includes
  • %:replace-outfile(X Y) replaces X by Y

It is not possible to modify an existing spec directly (override is possible but not append), therefore, first the existing spec is renamed and then a new spec with the same name is created and the old spec is included (first or last). Thus, effectively additional parameters can be appended or prepended. You will see this in nano.specs and in nosys.specs.

nano.specs

nano.specs contains this:

%rename link nano_link
%rename link_gcc_c_sequence nano_link_gcc_c_sequence
%rename cpp_unique_options nano_cpp_unique_options

*cpp_unique_options:
-isystem =/include/newlib-nano %(nano_cpp_unique_options)

*nano_libc:
-lc_nano

*nano_libgloss:
%{specs=rdimon.specs:-lrdimon_nano} %{specs=nosys.specs:-lnosys}

*link_gcc_c_sequence:
%(nano_link_gcc_c_sequence) --start-group %G %(nano_libc) %(nano_libgloss) --end-group

*link:
%(nano_link) %:replace-outfile(-lc -lc_nano) %:replace-outfile(-lg -lg_nano) %:replace-outfile(-lrdimon -lrdimon_nano) %:replace-outfile(-lstdc++ -lstdc++_nano) %:replace-outfile(-lsupc++ -lsupc++_nano)

*lib:
%{!shared:%{g*:-lg_nano} %{!p:%{!pg:-lc_nano}}%{p:-lc_p}%{pg:-lc_p}}

Thus, nano.specs effectively:

  • prepends -isystem=/include/newlib-nano to cpp_unique_options, this adds <toolchain_sysroot>/include/newlib-nano to the directories to be searched for headers
  • appends -lc_nano -lnosys in a group to link_gcc_c_sequence spec
  • replaces the standard libraries (-lc) with _nano versions (-lc_nano) in the link spec
  • overrides the lib spec to use nano versions of libraries

Since nano.specs modifies both cpp_unique_options and other linker related specs, it is used both for compiling and linking.

newlib-nano

On the Arm GNU Toolchain download page, there is a Linaro ABE manifest file with newlib and a Linaro ABE manifest file with newlib-nano that describes how the projects in Arm GNU Toolchain is built. The only difference between these is how newlib is built (normal vs. nano). The main difference is using --enable-newlib-nano-malloc and --enable-newlib-nano-formatted-io but there are also other differences listed below:

--disable-newlib-fseek-optimization
--disable-newlib-fvwrite-in-streamio
--disable-newlib-unbuf-stream-opt
--disable-newlib-wide-orient 
--enable-lite-exit 
--enable-newlib-global-atexit 
--enable-newlib-nano-formatted-io 
--enable-newlib-nano-malloc 
--enable-newlib-reent-small

and the following are common to both builds:

--disable-newlib-supplied-syscalls 
--enable-newlib-reent-check-verify 
--enable-newlib-retargetable-lockin

whereas the normal newlib library also includes these:

--enable-newlib-io-long-long 
--enable-newlib-io-c99-formats 
--enable-newlib-mb 
--enable-newlib-register-fini

thus, there is more difference than just using nano version of malloc and formatted io (stdio).

The detailed description or limitations of nano formatted io can be found in the newlib README.

An important option here is probably --disable-newlib-supplied-syscalls. When this is disabled, libcfunc.c, trap.S and syscalls.c are not included. These are under <toolchain_source>/newlib-cygwin/newlib/libc/sys/arm. Not exactly sure what newlib supplied syscalls mean but there is code for semihosting in these files. I guess to make a “plain” standard C library, they have to be disabled.

nosys.specs

nosys.specs contains this:

%rename link_gcc_c_sequence nosys_link_gcc_c_sequence

*nosys_libgloss:
-lnosys

*nosys_libc:
%{!specs=nano.specs:-lc} %{specs=nano.specs:-lc_nano}

*link_gcc_c_sequence:
%(nosys_link_gcc_c_sequence) --start-group %G %(nosys_libc) %(nosys_libgloss) --end-group

Thus, nosys.specs, effectively modifies link_gcc_c_sequence and appends the following in a group:

  • -lc_nano if nano.specs is given otherwise -lc
  • -lnosys

Since nosys.specs modifies only link_gcc_c_sequence spec, it is used only for linking.

libnosys

The source code of libnosys.a can be found in Arm GNU Toolchain source code /newlib-cygwin/libgloss/libnosys. The implementation is pretty clear, it returns error for almost all calls. For example, _open (in open.c) is implemented as:

int
_open (char *file,
        int   flags,
        int   mode)
{
  errno = ENOSYS;
  return -1;
}

It implements the following calls similarly, all returns the same error, ENOSYS: _chown, _close, _execve, _fork, _fstat, _getpid, _gettod, _isatty, _kill, _link, _lseek, _open, _read, _readlink, _stat, _symlink, _times, _unlink, _wait, _write.

Additionally:

  • it implements _exit by causing a divide by 0 exception.
  • it implements _sbrk by using the end symbol declared by the linker.
  • it creates an empty environment as follows:
char *__env[1] = { 0 }; 
char **environ = __env;

As these calls have probably no meaning in a bare-metal program, it makes sense to implement them this way. However, when you have a bare-metal system where some or all of these might have a different job, you can implement them differently.

When you use nosys, you might see warnings like this:

writer.c:(.text._write_r+0x10): warning: _write is not implemented and will always fail

this is just a warning to not forget that the syscall (_write) is not a proper implementation and it will always fail.

Summary

  • a spec file adds, removes or modifies the command-line options of gcc, thus it modifies how a file is compiled, assembled or linked
  • nano.spec builds and links with the standard C library newlib-nano
  • nosys.spec links with libnosys.a having a default implementation for all required syscalls and almost all returns an error

Is it possible to not use nano.specs and nosys.spec and still have the same result ? Probably, it sounds like these should be enough but it requires testing.

  • for compile and assembly: add -isystem =/include/newlib-nano
  • for link: remove -Wl,--start-group -lc -lm -Wl,--end-group and add -Wl,--start-group -lc_nano -lnosys -Wl,--end-group

References