Ada Runtime Library

From OSDev Wiki
Jump to: navigation, search

This guide intends to serve as an introduction to the Ada run-time library, and as a guide to creating a basic Ada run-time library suitable for bare-metal development targeting the x86 platform.
For other architectures, AdaCore provides pre-made run-time libraries and board support packages targeting a variety of ARM/RISC-V boards. These are freely available on Github, as well as being packaged with their 'arm-elf' and 'riscv32-elf' compilers.

Note: This guide is specific to the GNAT Ada compiler, available from either the FSF as part of GCC, or as a stand-alone product from AdaCore. Additionally it assumes the use of the GPRBuild suite of utilities, which are freely available as part of AdaCore’s GNAT compiler or can be built from source.



The run-time library for a language is typically the standard library of functions responsible for implementing the interface with the underlying functionality exposed by the system. These are normally statically linked against the executable at compile-time.

The Ada run-time library is responsible for the implementation of the standard library defined in annexes A-H of the Ada Language Reference Manual. Not all annexes defined in the standard are required to be implemented for a specific platform.
The Ada run-time is the equivalent of C’s libc. However unlike the C standard the Ada standard does not explicitly define a differentiation between hosted and freestanding environments.

As with other systems programming languages the program being compiled is statically linked against the run-time library to produce the final executable. The compiler will attempt to ensure that only the required sections of the run-time are linked, so as to minimize the final binary size. In the case that only minimal or no standard library features are required, the approach recommended by AdaCore here is to use a run-time library implementing only the features required.

The GNAT compiler offers a facility for configuring the runtime-library used during the compilation process. This is done by either using the --RTS=example command-line switch, or by using the

for Runtime ("Ada") use "example";

directive in the project’s configuration file.
This can be either the name of a run-time library that is visible to the compiler due to being in one of its search directories, or a relative path specifying the location of a run-time library directory.

As mentioned above, the Ada standard does not explicitly define a differentiation between hosted and freestanding environments. So unlike using GCC’s -ffreestanding switch to compile C for a bare-metal target, GNAT cannot automatically provide users with a valid run-time library suitable for targeting non-hosted environments. Bare-metal targets will often require fine-grained control to satisfy the limitations of the platform, so the developer will have to provide a run-time purpose built for the freestanding environment. For many targets there are Zero Foot Print' (zfp) run-time libraries available which are suitable for the purpose of operating-system development, but for x86 there are no zfp run-times freely available from the major vendors so the best option for a developer is to implement one themselves.

Anatomy of a run-time library

An Ada run-time library is fundamentally a directory tree containing the run-time sources and compiled library files. The adainclude subdirectory contains the run-time sources, which will provide the Ada equivalent of C header files for our run-time. The adalib subdirectory contains the compiled library artifacts that programs will be statically linked against. The compiler will require the presence of these two directories within the run-time directory to detect it as valid. The location of these directories can be customised by creating two files named 'ada_source_path' and 'ada_object_path' which allow the specification of alternate locations for the source and object directories. For more information on this feature, refer to the GNAT documentation.
A run-time library may be placed within the GNAT compiler's default search directories which will make it globally visible for the compiler, or can be placed in another location and specified as a relative path within the project's configuration file.
Note: GPRBuild requires the presence of a run-time within its search path before it will fully acknowledge the presence of GNAT for a particular target.

Creating a run-time library suitable for x86 operating-system development

Note: This section assumes that you are using an x86/x86-64 host compiler and are targeting an x86 system. Creating a run-time for other targets will be different. This section also assumes that you have a suitable cross-compiler. While you may be able to copy run-time source files from your system GNAT installation, a compiler specific to your target system will still be required. A detailed guide to building a GNAT cross-compiler can be found here.

In order to compile Ada code for bare-metal targets using the GNAT compiler, developers will need to configure an appropriate Ada run-time library for their target platform. The Ada standard recommends that all of the packages defined in ‘Annex A: Predefined Language Environment’ be defined for a platform, but for the purposes of bare-metal development this is impractical.
In practical terms, creating a new run-time for a particular target is accomplished through the creation and compilation of a static library. Control over which library units are included in the final run-time library is as simple as including and omitting the appropriate files from the library’s source tree.
For the purposes of operating-system development, at a bare-minimum the run-time library will need to implement parts of Annex C: ‘Systems Programming’. The terminology used within the Ada community for such a run-time library suitable for bare-metal is a 'zfp' run-time, short for 'Zero Foot Print'.

The best approach to creating a run-time library for a new platform is to start with components from the standard run-time library of your system's Ada compiler. Many of these components are platform independent and can be safely copied to your new run-time without further refinement.

Shown below is an example directory structure for a run-time library GNAT project:

- runtime
    - build             ( The build directory. Contains the final built run-time library. )
        - adainclude    ( Contains the run-time sources to be included in applications. )
        - adalib        ( Contains the compiled run-time static library. )
    - obj               ( The intermediate build directory. Contains build artifacts. )
    - source            ( Contains the run-time library source files. )

An example GNAT project configuration file for creating a run-time library can be seen below. The comment annotations will explain the relevant options.
For more information regarding GNAT project configuration refer to the AdaCore documentation: [1]

library project Runtime is
   --  Tells Gprbuild to create any missing directories in the build process.
   for Create_Missing_Dirs use "True";
   --  The source files for the run-time library.
   --  The 'build' directory is used in this case so that GNAT can use our
   --  run-time as the selected run-time library during the build process. 
   --  To facilitate this, we copy our source files to the build directory.
   --  This serves two purposes, one is to build the run-time itself, the 
   --  second is to function as the run-time library's spec includes.
   for Source_Dirs use ("build/adainclude");
   --  The directory used for build artifacts.
   for Object_Dir use "obj";
   for Languages use (
   package Builder is
      for Global_Configuration_Pragmas use "runtime.adc";
      for Switches ("Ada") use (
   end Builder;
   --  For a list of all compiler switches refer to:
   package Compiler is
      for Default_Switches ("Ada") use (
        --  Using these directives instructs the Gnat GCC backend to place
        --  each function and data object in its own section in the resulting
        --  object file. GNAT requires this in order for the linker to 
        --  perform dead code elimination within the runtime library.
        --  For more information refer to:
        --  This switch enables GNAT's Internal implementation mode. This 
        --  allows us to modify internal packages such as 'Ada' or 'System'
        --  which are needed to build a run-time library. This switch is only
        --  needed when building the run-time and should be disabled for all 
        --  other development.
        --  Instructs GNAT to remove dead code from the runtime library.
        --  Requires the use of the '-ffunction-sections' and
        --  '-fdata-sections' directives as detailed above.
   end Compiler;
   --  The destination directory for the final library artifacts.
   --  We need to select a static library for this purpose to ensure that the 
   --  run-time is linked staticly againt our executable.
   --  For more information on library project options refer to: 
   for Library_Dir use "build/adalib";
   for Library_Kind use "static";
   for Library_Name use "gnat";
   --  We require a valid run-time library to build our run-time. 
   --  We need to ensure that the run-time we use for this purpose is built 
   --  using the same compiler and targets the same platform. A reliable way
   --  to do this is to use our run-time's sources as a run-time to build
   --  itself.
   for Runtime ("Ada") use "build";
   for Target use "i686-elf";
end Runtime;

As noted in the source code comments above, in this setup the run-time sources are used as a run-time library during the build process. One method to implement this is to include a pre-requisite step in your makefile to copy the sources to the final build destination adainclude directory, then to specify this build directory as the run-time for the project. An example makefile for facilitating this can be seen below.

Please note that this makefile may not be fit for all projects and purposes.

RUNTIME_BINARY := ${LIB_DIR}/libgnat.a
RUNTIME_PROJ   := runtime
SRC_DIR        := src
BUILD_DIR      := build
LIB_DIR        := ${BUILD_DIR}/adalib
INCLUDE_DIR    := ${BUILD_DIR}/adainclude
LINKER_SCRIPT  := x86-multiboot.ld
RUNTIME_CONFIG := runtime.xml
.PHONY: clean
	gprclean -P${RUNTIME_PROJ}
	rm -rf ${BUILD_DIR}
# Build the run-time library.
	gprbuild -P${RUNTIME_PROJ}
# Copy all Ada source files to the final include directory.
${INCLUDE_DIR}/*.ad[sb]: ${SRC_DIR}/*.ad[sb] ${COMMON_SRC_DIR}/*.ad[sb] ${INCLUDE_DIR}
	cp -a ${SRC_DIR}/*.ad[sb] ${INCLUDE_DIR}
# Copy any Assemblersource files to the final include directory.
	cp -a ${SRC_DIR}/*.S ${INCLUDE_DIR}
# Copy the linker script to the final run-time directory.
# Copy our run-time config to the final run-time directory.
# Create the necesary directory tree.
	mkdir -p ${INCLUDE_DIR}
	mkdir -p ${LIB_DIR}

We can begin creating the source code for a minimal run-time by copying a list of files from our compiler's native run-time directory into the source directory of our run-time GNAT project. If you're using FSF GNAT this is most likely in a similar location to: /usr/lib/gcc/x86_64-linux-gnu/<version>/adainclude/. If you're using AdaCore GNAT Pro the directory structure for run-time sources is broken down into different sections, but you should still be able to find the same required files. Note: If you intend to use source files obtained from AdaCore's GNAT Pro or associated material in the construction of a run-time library, be sure to read their licensing to understand any restrictions imposed upon the use of their open-source material.
A list of files to copy from your compiler's default hosted run-time to your run-time library project's source directory are found below, together with an explanation of their purpose.

Filename Description Package specification for the Ada.Unchecked_Conversion package. This package provides a facility for unchecked casting from one type to another. This is generally discouraged in Ada, but can be necessary in bare-metal development.

You may notice the pragma Import (Intrinsic, Ada.Unchecked_Conversion); line here. This tells us that the body of this procedure is an *intrinsic subprogram* within the GNAT compiler and a definition is supplied by GNAT which we can import. For more information regarding intrinsic subprograms refer to AdaCore's documentation here: [2] The spec for the Ada package. This is the main parent package for most of the run-time library and is required to be present. The spec for the Interfaces package. This package defines the necessary data types and functions for interfacing with other languages. This is of particular note to bare-metal platforms, since it provides definitions of the Integer_X and Unsigned_X types. It also provides definitions of the intrinsic shift and rotate functions to work on these types. The spec for the System.Machine_code package. This package provides functionality for using inline assembly. This is relevant to operating-system development since it allows us more control over functionality like Port I/O. GNAT uses the same format for inline assembly as the rest of GCC. However it has additional features and syntax. The spec for the System.Storage_Elements package. This package provides functionality for using and manipulating memory addresses, which we will require for memory-mapped I/O.
s-stoele.adb The body for the System.Storage_elements package. See above. Contains the definitions for the functions contained in the spec. The spec for the System.Unsigned_Types package. Contains definitions for unsigned integer types. This is the spec file for the System package, which is the parent package for many of the packages in our run-time library. Every Ada program requires the System package to be present. This package contains important information about the target system, which is referenced by the compiler during code generation.

Refer to the 'System Implementation Parameters' section within the file for a list of parameters read by the compiler. For additional information regarding how these parameters are interpreted, refer to the following file within the compiler directory: gcc-<version>/gcc/ada/
Additional changes are needed to be made to this file to make it suitable for a bare-metal target. These changes are detailed in the section below.

Every Ada program is required to have access to the System package. This package provides a specification for the target system hardware to the compiler. Since a run-time is specific to a single target hardware configuration, there will only be one system specification per run-time.
Detailed below are the necessary changes required to convert the System package specification from a hosted run-time to a freestanding one suitable for kernel development. For more information on individual settings refer to this file within your GNAT installation: gcc-<version>/gcc/ada/ A copy of this file can be found online at: [3]

Change Description
Command_Line_Args := False Since our platform is bare-metal, there is no access to command line parameters.
Configurable_Run_Time := True Instructs GNAT that this is a user-configured run-time library.
Exit_Status_Supported := False As above, our platform does not support exit statuses.
Stack_Check_Probes := False Indicates that our run-time does not use GCC's stack checking mechanism.
Suppress_Standard_Library := True According to the GNAT docs, If this flag is True, then the standard library is not included by default in the executable.
ZCX_By_Default := False Indicates if zero cost exceptions are active by default. Refer to the documentation for more information on Zero-cost-exceptions.
GCC_ZCX_Support := False Disables GCC's support for zero-cost exceptions.

Also, add the following line in the private part of the package:

   Run_Time_Name : constant String := "Bare Bones Run Time";

According to, this directive should be the first thing after the private keyword.

Platform-specific support code

For the purposes of bare-metal development, it may be advantageous to define platform-specific support packages within the run-time library to avoid cluttering the main kernel package with platform-specific code. This also supports a greater degree of abstraction, and better portability of the kernel itself.
Any packages contained within the runtime will be accessible to the linked program automatically. Be mindful not to declare any new packages as children of the top level Ada or System packages, as GNAT will consider any non-standard children to be ‘implementation-specific’: for internal use only and forbid their use outside of the run-time.

It might be advantageous in certain situations to declare platform-specific support packages as children of the System package to restrict the direct use of platform-specific code in the linked application.

One example of useful platform support code for x86 would be to implement functionality for Port IO. An example implementation can be seen below.

--  X86
--  Purpose:
--    This package contains support code for the x86 system.
package x86 is
   pragma Preelaborate (x86);
end x86;

with Interfaces;
with System;
--  X86.PORT_IO
--  Purpose:
--    This package contains functionality for port-mapped I/O on the x86
--    platform.
--    Functions are included for inputting and outputting data to port-mapped
--    addresses, useful for interacting with system peripherals.
package x86.Port_IO is
   pragma Preelaborate (x86.Port_IO);
   --  Inb
   --  Purpose:
   --    This function reads a byte from a particular IO port.
   --  Exceptions:
   --    None.
   function Inb (
     Port : System.Address
   ) return Interfaces.Unsigned_8
   with Volatile_Function;
   --  Outb
   --  Purpose:
   --    This function writes a byte to a particular IO port.
   --  Exceptions:
   --    None.
   procedure Outb (
     Port : System.Address;
     Data : Interfaces.Unsigned_8
end x86.Port_IO;


with System.Machine_Code;
package body x86.Port_IO is
   --  Inb
   function Inb (
     Port : System.Address
   ) return Interfaces.Unsigned_8 is
      Data : Interfaces.Unsigned_8;
      System.Machine_Code.Asm (
        Template => "inb %w1, %0",
        Inputs => (
          System.Address'Asm_Input ("Nd", Port)
        Outputs => (
          Interfaces.Unsigned_8'Asm_Output ("=a", Data)
        Volatile => True);
      return Data;
   end Inb;
   --  Outb
   procedure Outb (
     Port : System.Address;
     Data : Interfaces.Unsigned_8
   ) is
      System.Machine_Code.Asm (
        Template => "outb %0, %w1",
        Inputs => (
          Interfaces.Unsigned_8'Asm_Input ("a", Data),
          System.Address'Asm_Input ("Nd", Port)
        Volatile => True);
   end Outb;
end x86.Port_IO;

Fundamentally, the role of the runtime library is to implement the features defined in the Ada language standard for the particular target platform. This requires the creation of target-specific packages to interface with platform features such as internal clocks, UARTs and timers, among other things. These packages can then be used by language-defined packages in the runtime library to expose this functionality to the linked application.

System initialization code

It is possible to perform the initialization of the underlying hardware platform within the run-time library.
AdaCore GNAT’s ARM bareboard target run-times employ this method of system initialization, packaging the run-time library as a combined ‘board support package’.
These run-times typically include a linker script containing a memory map of the target as well as assembly code for initializing the target platform. These linker scripts are not used in the building of the run-time library itself, but are invoked when a program is linked against the run-time library. This is facilitated via the use of additional configuration files bundled with the run-time library.
GNAT/Gprbuild is capable of building Asm/C files bundled inside a GNAT project, which allows us to include platform initialization code. XML configuration files can be used to provide additional configuration options to Gprconfig when linking against the run-time library. These XML files are generally referred to in official documentation as gprbuild’s ‘knowledge base’. In addition to its own directory, Gprbuild will search the source directories for the program being compiled, as well as its run-time library directories. To see what directories are searched for source and configuration files, the ‘gprls’ utility can be used. Example usage is gprls -v -P project.gpr.

A practical usage of this would be to define a runtime.xml file, placed within the final adainclude directory of the run-time library. This is used to automatically use a linker script for the target platform when the kernel is linked against the library.

<?xml version="1.0" ?>
        package Linker is
            for Required_Switches use Linker'Required_Switches & (
        end Linker;

Where ${RUNTIME} is a predefined variable made available by Gprbuild.

Additional documentation for these features can be found at: [4]


Exception handling in Ada is implemented in the run-time library package Ada.Exceptions. This is defined in section 11 of the the Ada Language Reference Manual and can be referenced here. Comprehensive exception handling functionality is typically disabled for bare-metal targets due to the large amount of underlying functionality required for its implementation, such as stack unwinding, streams, secondary stack, etc. As such it is not normally included in the run-time library for such targets. Local handling exceptions within a subprogram is still possible without the Ada.Exceptions package being implemented for a target.

For a bare-bones run-time library used for operating-system development it is recommended to use the No_Exception_Propagation restriction pragma. This restriction ensures that an exception is never propagated over a subprogram boundary into an outer scope. The implementation of exception handling over subprogram boundaries typically requires the use of setjmp/longjmp, which are unsuitable for the purpose of bare-metal development. With this restriction applied the only case in which an exception may be raised is when the exception handler is contained within the same subprogram. According to official documentation, the effect of this is that raising an exception is essentially a goto statement.

Any exception not handled explicitly within the same subprogram will be caught by the Last_Chance_Handler subprogram.


Ada refers to the expansion of definitions contained within package files as elaboration. The Ada standard defines the pragma directive Preelaborate to specify that a package requires no elaboration at run-time. This is ideal for our purposes since our run-time library will be instantiated and initialized at boot time, prior to having a fully functional system environment. At this point we have limited facilities for initializing run-time library components. Marking our run-time library components as pre-elaborable ensures that they do not require any additional execution at run-time, any violation of this pragma will generate a compile-time error. The legality rules for use of this directive can be found in the Reference Manual: [5]

The language standard also defines the ‘pure’ pragma. This pragma specifies that any call to any of the package’s units should be considered to have no side-effects. This is ideal in many circumstances, but a developer would be advised to be wary that this pragma advises the compiler that it may cache subsequent calls to the same package unit. As a result this pragma directive is not suitable for a package using any volatile memory access.

Additional documentation on this subject is available from AdaCore at: [6]

Image attributes

Scalar types in Ada support the Image attribute. This attribute returns a string representation of a variable. This is of particular significance for operating-system development due to the need to ouput debugging information in string format to the console. The Image attribute is defined in Annex K of the Ada Language Reference Manual. Support for the image attributes of a particular scalar type is implemented in the run-time library.
For operating-system development, arguably the most important types to be able to print are unsigned integer types. By default, the GNAT compiler expects functionality for imaging unsigned integer types to be defined in the package and its associated package body. Provided below is an example implementation:

with System.Unsigned_Types;
--  Purpose:
--    This package provides an implementation of the Image attribute for
--    unsigned integer types.
package System.Img_Uns is
   pragma Pure;
   --  Image_Unsigned
   --  purpose:
   --    Computes Unsigned'Image (V) and stores the result in S (1 .. P) \
   --    setting the resulting value of P. The caller guarantees that S is
   --    long enough to hold the result, and that S'First is 1.
   procedure Image_Unsigned (
     V :        System.Unsigned_Types.Unsigned;
     S : in out String;
     P : out    Natural
   with Inline;
   --  Set_Image_Unsigned
   --  Purpose:
   --    Stores the image of V in S starting at S (P + 1), P is updated to
   --    point to the last character stored. The value stored is identical
   --    to the value of Unsigned'Image (V) except that no leading space is
   --    stored. The caller guarantees that S is long enough to hold the
   --    result. S need not have a lower bound of 1.
   procedure Set_Image_Unsigned (
      V :        System.Unsigned_Types.Unsigned;
      S : in out String;
      P : in out Natural
end System.Img_Uns;


with System.Unsigned_Types; use System.Unsigned_Types;
package body System.Img_Uns is
   --  Image_Unsigned
   procedure Image_Unsigned (
     V :        System.Unsigned_Types.Unsigned;
     S : in out String;
     P : out    Natural
   ) is
      pragma Assert (S'First = 1);
      S (1) := ' ';
      P := 1;
      Set_Image_Unsigned (V, S, P);
      when Constraint_Error =>
   end Image_Unsigned;
   --  Set_Image_Unsigned
   --  Implementation Notes:
   --    - Refer to:
   --      writing-linux-modules-in-ada-part-3/
   procedure Set_Image_Unsigned (
     V :        Unsigned;
     S : in out String;
     P : in out Natural
   ) is
      --  The number of digits in the resulting string.
      Digit_Count : Natural := 0;
      Get_Digit_Count :
            V2 : Unsigned := V;
            while V2 /= 0 loop
               Digit_Count := Digit_Count + 1;
               V2 := V2 / 10;
            end loop;
            when Constraint_Error =>
               Digit_Count := 0;
         end Get_Digit_Count;
      Write_To_String :
            if Digit_Count = 0 then
               P := P + 1;
               S (P) := '0';
               for I in reverse 0 .. (Digit_Count - 1) loop
                  P := P + 1;
                  S (P) := Character'Val (48 + (V / 10 ** I) rem 10);
               end loop;
            end if;
            when Constraint_Error =>
         end Write_To_String;
   end Set_Image_Unsigned;
end System.Img_Uns;

User-defined images for a given type can be implemented by overriding the default implementation of the Put_Image attribute.


The Ada run-time library is initialized by calling the adainit procedure. This procedure is responsible for the elaboration of the individual packages within the run-time library.

It is the responsibility of the platform initialization code to call this procedure prior to using any of the subprograms in the run-time library.
As noted here " is assumed that the basic execution environment must be setup to be appropriate for Ada execution at the point where the first Ada subprogram is called. In particular, if the Ada code will do any floating-point operations, then the FPU must be setup in an appropriate manner. For the case of the x86, for example, full precision mode is required..."

The Ada standard provides the following implementation advice: "If an implementation supports Export for a given language, then it should also allow the main subprogram to be written in that language. It should support some mechanism for invoking the elaboration of the Ada library units included in the system, and for invoking the finalization of the environment task. On typical systems, the recommended mechanism is to provide two subprograms whose link names are 'adainit' and 'adafinal'. Adainit should contain the elaboration code for library units. Adafinal should contain the finalization code. These subprograms should have no effect the second and subsequent time they are called." Additional documentation can be found here

In the case of a pure Ada kernel, a good place to put this procedure call is within the boot code assembly to ensure that the run-time is properly initialized prior to the jump to Ada code.

Additional Resources

Additional documentation on the subject of creating a new run-time library is available from AdaCore at: [7]

Information regarding porting AdaCore's ARM runtimes to a new system can be found here: [8]

Personal tools