GNAT Zero Foot Print - Take 2 - No C

06/07/18, modified 17/09/18

"Libc gotta go."

—Stanislav Datskovskiy

And it will. In an, at this moment, unknown amount of steps the C library can be ripped out from the Ada Runtime library and be replaced with Ada and assembly code. In the first step, all C calls need to be replaced with Ada code and possibly some assembly to perform system calls to the Linux kernel. The second step is then to replace the C library specific start-up code with code for Ada.

I start with the previous version of a minimal ZFP library for Linux. This library uses only two calls to the C library, one to output characters and the other to exit the code. Both are replaced with a direct system call1. The second change is to include a file with startup code2. The resulting code is published in the following vpatch (with signature).

Combine this patch with those from the previous installment, press and build it. Building the code needs to be done with the Makefile3.

<<create a directory and put a .wot directory in it with at least my key>>

v.pl init http://ave1.org/code/zfp

v.pl p a zfp_2_noc.vpatch

cd a

make

cd examples

make

All system calls can be found in the adainclude/s-syscal.adb file. The Write function (used for outputting characters) is implemented as a single assembly statement syscall with the parameter list specified to fill the right processor registers. The function starts with a conversion from characters to bytes4 and ends with a check of the return values. After a completed system call the 'RAX' register will be filled with a return code. If an error occurred during execution of the system call, the register will contain the error code as a negative number (always between -1 and -4096). If the execution was successful the register will contain 0 or any other 64bit number outside of the range -1 to -4096.

function Write (fd : in Int; S : in String; E : out ErrorCode) return Int is
    type byte is mod 2**8;
    B : array (S'Range) of byte;
    R : Int := 0;
 begin
    for I in S'Range loop
       B (I) := Character'Pos (S (I));
    end loop;
    Asm
      ("syscall",
       Outputs => (Int'Asm_Output ("=a", R)),
       Inputs  =>
         (Int'Asm_Input ("a", SYSCALL_WRITE),
          Int'Asm_Input ("D", fd),
          System.Address'Asm_Input ("S", B'Address),
          Int'Asm_Input ("d", B'Length)),
       Volatile => True);
    if R < 0 and R >= -(2**12) then
       E := ErrorCode'Val (-R);
       R := -1;
    else
       E := OK;
    end if;
    return R;
 end Write;

The a-textio.adb and last_chance_handler.adb files have been updated to use the system calls instead of the C library. The s-maccod.ads was added from the GNAT runtime library to support the inline assembly code. The other addition is the startup.S file. In it simplest working form it just needs to contain one definition of a global (_start), a call to a main function and a syscall to exit the code;

.global _start

_start:
  call main

  /* exit code */
  mov $60, %rax
  mov $0, %rdi
  syscall

The version in the patch also stores the argument count and a pointer to the argument array in two globals. Both globals are unused for now but will be needed for future parsing of any command line arguments.

The final noteworthy change is the inclusion of a runtime.xml file. The gprbuild command will use this file to set flags for all projects that are build with the runtime library. For reasons , this file is written as an xml file containing gprbuild project statements;

<?xml version="1.0" ?>

<gprconfig>
  <configuration>
   <config>
   package Linker is
      for Required_Switches use Linker'Required_Switches &amp;
        ("${RUNTIME_DIR(ada)}/adalib/libgnat.a") &amp;
        ("-nostdlib", "-nodefaultlibs", "-lgcc");

      for Required_Switches use Linker'Required_Switches &amp;
          ("${RUNTIME_DIR(ada)}/adalib/start.o");
   end Linker;

   package Binder is
      for Required_Switches ("Ada") use Binder'Required_Switches ("Ada") &amp;
       ("-nostdlib") ;
   end Binder;
   </config>
  </configuration>
</gprconfig>

The linker flags are set so that no standard C library or startup code is included in the resulting binary. As we are then lacking the default startup code, an extra line is added to include the start.o code with every compile.

In the end, the fun part, a working binary. The hello world example from the previous installment can be built and it's size inspected. It is now at 2.6k (down from 54k) on my computer5.

In the final end, I will include another reference to AdaCore's configurable runtime documentation. The GNAT documentation has been very helpful for learning the GNAT system and developing this library.

  1. The main difficulty in doing so is to learn how the Linux system calls work and get a better understanding of the inline assembly statements. Stans' demo.asm posted in the logs proved very helpful for this process []
  2. This file is now written in assembly, although (upon reflection) it should be possible to rewrite it in Ada []
  3. I did not find a method to compile one separate file into an object file with gprbuild []
  4. Which in practice will be a copy operation []
  5. Ofcourse, this minimal library is too minimal. In some cases (for example when a string is concatenated) the compiler will generate memcpy or memset calls. We need to provide replacement Ada functions for each. This is not difficult as the ada 2017 code contains pure Ada versions for all of these. []

2 Responses to “GNAT Zero Foot Print - Take 2 - No C”

  1. [...] best way to use Keccak hashes as part of his own V implementation and Arjen (ave1) has released a zero foot print (ZFP) library that is delightfully effective at eliminating bloat. And on top of all this, V itself is moving [...]

  2. [...] and glibc-free GNAT. On top of this, at the time of this writing, he is also working on a entirely delibraryized GNAT, which will produce compact and deterministic — i.e. smoothly hand-auditable [...]

Leave a Reply