Demystifying Arm GNU Toolchain Specs: nano and nosys
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 withlibc_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 withlibnosys.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 withlibrdimon*.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 withlibgloss-linux.a
.nano.specs
: for using_nano
standard C librariesnosys.specs
: for compiling to bare-metal, used withlibnosys.a
.rdimon*.specs
(both): used for semihosting withlibrdimon*.a
rdpmon.specs
: used for semihosting withlibrdpmon.a
.redboot.specs
,iq80310.specs
andpid.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 libraryg
: standard C library with debug enabledgfortran
: Fortran shared librarygloss-linux
: library for using Linux syscallsm
: 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 applicationsrdimon
: remote debug interface monitorrdpmon
: remote debug protocol monitorstdc++
: standard C++ librarysupc++
: 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 linkerlib
: 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 fileslink_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 theold
spec tonew
*spec
adds, modifies or removes the spec depending on the following lines. If the result of following lines are empty, then thespec
is removed.%{S:X}
means, if-S
is given to GCC, it is replaced withX
. Pay attention the first has no-
.%(spec)
means to include whatever thespec
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
tocpp_unique_options
, this adds<toolchain_sysroot>/include/newlib-nano
to the directories to be searched for headers - appends
-lc_nano -lnosys
in a group tolink_gcc_c_sequence
spec - replaces the standard libraries (
-lc
) with_nano
versions (-lc_nano
) in thelink
spec - overrides the
lib
spec to usenano
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
ifnano.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 theend
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 ofgcc
, thus it modifies how a file is compiled, assembled or linked nano.spec
builds and links with the standard C librarynewlib-nano
nosys.spec
links withlibnosys.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
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.