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 -mthumbAssembler:
-mcpu=cortex-m33 -c -x assembler-with-cpp --specs=nano.specs -mfloat-abi=soft -mthumbLinker:
-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.Srather 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-sectionsand-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_nanostandard C librariesnosys.specs: for compiling to bare-metal, used withlibnosys.a.rdimon*.specs(both): used for semihosting withlibrdimon*.ardpmon.specs: used for semihosting withlibrdpmon.a.redboot.specs,iq80310.specsandpid.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 newrenames theoldspec tonew*specadds, modifies or removes the spec depending on the following lines. If the result of following lines are empty, then thespecis removed.%{S:X}means, if-Sis given to GCC, it is replaced withX. Pay attention the first has no-.%(spec)means to include whatever thespecincludes%: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-nanotocpp_unique_options, this adds<toolchain_sysroot>/include/newlib-nanoto the directories to be searched for headers - appends
-lc_nano -lnosysin a group tolink_gcc_c_sequencespec - replaces the standard libraries (
-lc) with_nanoversions (-lc_nano) in thelinkspec - overrides the
libspec to usenanoversions 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_nanoifnano.specsis 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
_exitby causing a divide by 0 exception. - it implements
_sbrkby using theendsymbol 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
specfile adds, removes or modifies the command-line options ofgcc, thus it modifies how a file is compiled, assembled or linked nano.specbuilds and links with the standard C librarynewlib-nanonosys.speclinks withlibnosys.ahaving 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-groupand 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.