GNAT and Zero Foot Print Runtimes

Executables written in ADA and compiled with GNAT, include a portion of the GNAT runtime library, the C library and some start-up/shut-down code. The total sum of extra code bytes added to any statically compiled executable can easily become 2 megabytes. Most of this code will never be called. Good luck to you, if you want to read the decoded assembly lines of such an executable. As the same luck would have it, the GNAT system includes the possibility to compile code with a custom runtime library and GCC itself can use different C libraries.

The gnat compilers can use alternate GNAT runtimes. The most common use of such alternate runtimes is for embedded systems with little or no OS. A guide to building such an embedded runtime system can be found here12. This guide provided the basis for the runtime described here.

A minimal runtime can be found in the following vpatch;

The vpatch needs to be pressed, and the code can be build with gprbuild.

All optional code has been removed from the gpr project file3.

library project Gnat_Runtime is
   for Languages   use ("Ada");

   for Source_Dirs use ("adainclude");
   for Object_Dir use "obj";
   for Library_Kind use "static";
   for Library_Name use "gnat";
   for Library_Dir use "adalib";

   package Builder is
      for Default_Switches ("Ada") use (
              "-gnatg", "-gnatyN",
              "-gnatec=" & Gnat_Runtime'Project_Dir & "restrict.adc");
   end Builder;

   package Compiler is
      for Default_Switches ("Ada") use (
   end Compiler;

end Gnat_Runtime;

If you inspect the pressed directory, you can see that a GNAT runtime is a directory with two subdirectories adainclude and adalib. The adainclude directory needs to contain the ada library files, the adalib needs to contain the libgnat.a and the .ali files. The obj directory is for intermediary build products and can be safely deleted after a build. Notice that the two sub-directories adainclude and adalib are all that is needed for a GNAT runtime.

├── adainclude
│   ├──                  -- The Ada package (Pure, empty, just the root)
│   ├── a-inteio.adb             -- Ada.Integer_Text_IO (for Put function of an Integer)
│   ├──             -- Ada.Integer_Text_IO
│   ├── a-textio.adb             -- Ada.Text_IO (a few functions (Put, New_Line) for standard output)
│   ├──             -- Ada.Text_IO
│   ├──             -- For interaction with C
│   ├── last_chance_handler.adb  -- No exception handling, so a last_chance_handler is needed
│   ├──  --
│   ├── s-elaall.adb             -- Elaboration code, see file for explanation
│   ├──             --
│   ├── s-parame.adb             -- Parameters for the C interface, needed by the last_chance_handler
│   ├──             --
│   └──               -- The configuration of the runtime
├── adalib                       -- Output directory, will contain libgnat.a
│   └── README                   -- Placeholder
├── gnat_runtime.gpr             -- The gprbuild project file
├── obj                          -- Output directory for intermediate results
│   └── README                   -- Placeholder
├── README                       -- Very small explanation
└── restrict.adc                 -- Restrictions

The "" file is paramount in configuring any GNAT runtime library. This file is well documented in AdaCore's configurable runtime documentation. That document describes the AdaCore Pro code, it is also valid for the GPL version4. The file is also the file to put restrictions in, these restrictions will be valid for the runtime code and all code compiled with the runtime.

The "|adb" files are necessary as the default exception handling mechanism is not included in this runtime (and the "|adb" are needed for the last_chance_handler). The handler function needs to eat C strings, so some very ugly Ada code is needed here5.

I've written two very basic examples to test this runtime6, one to test the size of the generated executable and one to test if the error handling still works as expected. These examples can be found in the following patch:

The examples can be build with the project file in the examples subdirectory. The examples work with the default runtime and the zfp runtime. To build with the zfp runtime do gprbuild --RTS=../. The example can be build with a glibc based gnat (GNAT AdaCore 2016) and a musl based gnat (using the MUSL build instructions), a small table;

C Library GNAT Runtime Executable Size (Kbytes) Stripped Size (Kbytes) Comments
GNU default 1226 851  
GNU zfp 962 738
MUSL default 669 122
MUSL zfp 60 54
  1. A very basic C hello world program also compiles into an executable of around 1Mb. Running nm on the generated binary is unnerving, it seems like a large portion of the gnu library is included by default. I've compiled the gnu library from source with the dynamic NSS library support turned off, this does not give any improvement. I've edited the startup code in the gnu c library, also without improvement on this point7.
  2. This needed a patch in the build process, by default the gnat library will not build with function and data sections8. Without these sections the executable size was 1641 Kbytes. The AdaCode GNAT 2016 static library was build with function sections and without any debug information9.
  3. It may be possible to get this even smaller, with the right options and restrictions the GNAT specific initialize and finalize functions can be removed. Maybe editing gnatbind and then recompiling could also result in less bytes. Both seem to me to be too much effort for the expected gain.

As it turns out, if only a limited set of the Ada library is employed, the default GNAT runtime does not add that much extra code to an executable.

Next, the '"constraint/constraint.adb"' file. The code in this file is designed to trigger a constraint error. For every constraint error the code should end up in the last_change_handler function. This is easily tested by running the constraint binary10.

  1. A WIKI, these still exist!. And it can only be accessed over HTTPS. Why?? []
  2. AdaCore also publishes code to generate Zero Foot Print runtimes or Bare metal BSPs. I could not get this script to work for an linux system. The code seems to support it, but no. []
  3. An alternative runtime library needs to be build with the -gnatg flag. This flag has a number of effects, one of them is to handle all warnings as if these where errors. Another is to enable style checking warnings. If you do not want to follow the GNAT style, these checks can be undone again with -gnatyN. []
  4. The document describes Zero Cost Exceptions, these are zero cost in time overhead, not in space, the amount of code this adds is not small. Also, as it is gcc backend code, you get gcc code, probably the same used for C++ exception handling. For less complexity and better understanding of the code, do without the whole Ada exception handling mechanism. []
  5. It makes sense to write the last_change_handler function in C. Unfortunately making the project an Ada + C project will result in a broken library. Some flags are removed from the compilation phase. These can be put back by updating the project file but so far that did not help. []
  6. I can report that FFA chapters 1 to 3 also work with the runtime []
  7. Some more things discovered, it is not possible to build just a static library by default. Also the library needs to be build with optimization on or the build will fail []
  8. To determine if a library was compiled with "-ffunction-sections -fdata-sections", run objdump -t <libraryname>.a. Look at the section of a function, if it is called .text then it was not compiled with the flags, if it is called .text.<functionname> then it was compiled with the flags. []
  9. Compiling the GNAT library without debug information proved to be one of the harder parts. The "-g" flag is sprinkled in many of the Makefiles of gcc. As a temporary fix, the adalib was build separately from the compiler, this last part needs to be included in the MUSL build instructions []
  10. You might think of restriction pragma's as providing checks against the source code. These checks will limit the kind of constructs you are allowed to make in writing Ada code. This is true for a lot of the restrictions, but not for the 'No_Exceptions' restriction. The No_Exceptions restriction will turn off all constraint checking. See the Ada 2012 LRM []

3 Responses to “GNAT and Zero Foot Print Runtimes”

  1. Libc gotta go.

    There's not much point in this exercise if it's still in there, obscenely taking up 800+kB.

    For what does GNAT even use libc ? for memcpy?!

  2. ave1 says:

    I agree, GNU libc is only overhead. Indeed about 600K, this is mostly dlopen and other shit. GNAT by default uses GNU's libc init and exit functions. And those functions seem to want all the others, I've done a little work in removing calls and rebuilding glibc, but so far no luck.

    As far as MUSL libc goes, this is probably 10 / 20K or so, it is used for init and putchar etc. (and yes gnat will use memset, memcopy for aggregate statements, others => 0)

  3. What's dlopen doing in a statically linked elf ?

Leave a Reply to Stanislav Datskovskiy