AdaCore Blog

Bare-metal C++ development environment for certifiable safety-critical applications

by Jose Ruiz

Introduction

C++ can be an option for embedded systems, including safety-critical applications, when you control the code that ends up in the final executable and you trust the full software stack.

GNAT Pro supports C++ on bare-metal targets (no operating system) with a reduced version of the standard C++ run-time library needed for some parts of the core language, corresponding to a freestanding implementation of C++.

The C++ development environment has been designed to give users the freedom to choose their C++ subset, and hence their needs in terms of run-time support libraries; templates, abstract classes, static objects and dynamic dispatching are supported without any underlying C++ library. The support library for C++ that comes with GNAT Pro for C++ contains functionalities to deal with run-time type information (RTTI), exception handling, dynamic memory, and termination handlers, among other useful features.

C++ support is available with bare-metal GNAT Pro toolchains targeting popular hardware in the avionics, defense, railway, and space domains. It includes the following processor architectures: PowerPC (32 bits), x86 (64 bits), RISC-V (32/64 bits), LEON3 (32 bits) and ARM (32/64 bits).

Execution environment

We are talking about bare metal, so we need some code to start up the board and provide some basic services. We want something simple, restricting the memory footprint and complexity, with a basic Board Sup­port Pack­age (BSP) handling the start up and the memory layout.

For this blog I am using the Xilinx Zynq UltraScale+ MPSoC platform based on the 64-bit ARM Cortex-A53 processor.

Without entering into the details, the BSP will perform a minimal configuration of the processor, initialize the interrupt vector table, initialize the stack, cleanup the data that needs to be initialized to zero, execute global constructors, and then call the user’s code. This underlying support is enough for some core C++ features that can be used with no C++ run-time support and low overhead as explained in the following sections.

Static objects and dynamic dispatching

When talking about C++, the first things that come to my mind are polymorphism, dynamic dispatch, templates, constructors and destructors. These features, plus some others that constitute the core of the C++ language, can be used with GNAT Pro while no C++ run-time library is being used at all. Examples of these features are:

  • Static objects: including lifetime control with constructors and destructors
  • Abstract classes: with pure virtual functions
  • Templates: generic reusable code
  • Dynamic dispatch: extending the functionality and properties of existing classes through polymorphism

If we have a class hierarchy like the following:

// Abstract class
class Shape {
  public:
    virtual double GetArea() const = 0;
    virtual double GetPerimeter() const = 0;
};

class Circle: public Shape {
  private: double radius;

  public:
    Circle(double rad): radius(rad) {}

  double GetArea() const override {
    return PI * pow(radius, 2);
  }

  double GetPerimeter() const override {
    return 2 * PI * radius;
  }
};

You can then instantiate objects of these classes and use dynamic dispatch to invoke the corresponding methods.

bool BigSize(Shape *obj) {
  // Dispatching call
  return obj->GetArea() > 10.0;
}

// Static object
Circle my_circle(3.2);

if (BigSize(&my_circle)) {
  ...
}

Dynamic memory

As we have seen in the previous section, allocation and automatic deallocation of objects on the stack (and on the global data area) can be used without any run-time support.

Dynamic memory allocation is a core part of C++, giving a certain flexibility to application designers through the use of the new and delete operators. In many bare-metal applications its use is forbidden (to avoid dynamic memory vulnerabilities, to simplify the run-time libraries, and to reduce the cost of their potential verification and certification).

If this dynamic behavior is however needed, GNAT Pro comes with run-time libraries that support:

  • basic memory allocation on the heap (provided by the BSP), and
  • the C++ new and delete operators (as a C++ thin binding on top of the underlying dynamic memory management)

These two libraries give you the flexibility to dynamically create and destroy objects on the heap, including instances of classes.

Circle my_circle(3.2); // Allocated on the stack
Circle* circle_ptr = new Circle (2.6); // Allocated on the heap
...
delete circle_ptr; // Deallocate from the heap

Exceptions

Exceptions fall into the category of interesting key features of the C++ language, but with several drawbacks: 1) it increases the memory footprint with tables to unwind the stack, and 2) it relies on relatively complex code to perform exception propagation through the stack frames in search of the corresponding exception handler.

However, exception handling is a versatile mechanism for implementing graceful error recovery.

class exception {
  ...
};

void processing (void) {
  ...
  if (error)
    // erroneous situation
    throw exception();
  ...
}

try {
  processing ();
} catch (exception e) {
  //error recovery
  ...
}

If C++ exceptions are not needed, it is possible to control code generation to suppress the production of static unwind tables, and hence reduce the memory footprint, using the -fno-unwind-tables switch. The -fno-exceptions option removes anything that is needed to support code that either throws or catches exceptions. Obviously, the -fno-exceptions option will trigger an error when finding any code that tries to use any of these functionalities.

Run-time type information

Run-time type information is also one of the core features of C++, allowing retrieval of the object type information (using the typeid operator) as well as casting and checking the inheritance hierarchy (using dynamic_cast) at execution time.

The following code shows how to safely get the view of an object as a member of a given class, and how to query type information for objects.

Rectangle* rectangle_ptr = new Rectangle(4.3, 1,2);
Square* square_ptr = new Square(2.1);

Rectangle* tmp_ptr = dynamic_cast<Rectangle *>(square_ptr);

if (tmp_ptr != nullptr){
  //  Successful cast
  ...

  if (typeid(rectangle_ptr) == typeid(tmp_ptr)) {
    ...
  }
  ...
}

This functionality increases the memory footprint due to tables containing type information that can be queried at execution time, and implies run-time overhead for the virtual table lookup. The -fno-rtti option suppresses the generation of these tables and forbids the use of this functionality (raising an error in any code trying to use it).

Certification

Everything that was mentioned before about restricting the C++ run-time library was an effort not only to reduce the memory footprint but also to reduce complexity and facilitate the verification of the entire embedded application.

The complexity aversion goes to extremes in safety-critical applications where every single byte that ends up in the final binary needs to be verified and justified thoroughly. The run-time libraries are no exceptions to this rule because any error or misbehavior is a hazard that may have catastrophic effects on the final system.

The simplest option is to use the C++ features that need no run-time libraries at all, meaning that we avoid certifying any C++ support library. The combination of the following compiler switches ensures no implicit use of the C++ libraries, and restricts the code generation strategy to a minimum:

  • -fno-unwind-tables: suppress generation of static unwind tables
  • -fno-exceptions: no support to throw or catch exceptions
  • -fno-rtti: no run-time type information
  • -nostdlib: exclude standard libraries

However, GNAT Pro for C++ comes with a higher degree of flexibility, offering a streamlined version of the C++ run-time library (libsupc++) that contains functions dealing with run-time type information (RTTI), exception handling and dynamic memory. A reasonable certification strategy would be to trim down this library to the minimum required by the application and certify this subset.

Sustained branches

When your C++ project has the most stringent requirements for reliability, long-term maintenance, or certification, GNAT Pro for C++ offers a specialized service known as sustained branches that allows a project to continue its use of a specific version of the technology, including upgrades to repair critical issues.

If a tool defect has a critical impact (for example a code generation error affecting the customer software’s certification), a corrective release is provided, extremely close to the release in use, together with an impact analysis of the difference between the two versions, as required for certification.

Conclusion

GNAT Pro for C++ provides a versatile development environment for bare-metal targets capable of supporting different subsets of the C++ language, where users decide the run-time costs (in terms of memory footprint and execution-time overhead).

Many interesting core C++ features, like polymorphism, dynamic dispatch, templates, constructors and destructors, abstract classes, static objects, type traits, and atomic types, can be used with no C++ run-time support and an extremely low overhead.

A streamlined version of the C++ run-time library is available when run-time type information, exception handling, dynamic memory, or coroutines are of interest.

All these are in stock for your favorite processor architecture (PowerPC, x86, RISC-V, LEON3, ARM), including all services needed to ensure the success of your certification and long-lived safety-critical projects.

About Jose Ruiz

Jose Ruiz

Dr. Jose Ruiz is a Product Manager at AdaCore with 25 years of experience in embedded safety-critical real-time systems, having authored/coauthored over 40 papers in that area. He received his Ph.D. degree for his work in the field of real-time and multimedia systems, including scheduling policies and resource management in real-time operating systems.

He is an expert in certification of high-integrity system in aeronautics, space and railway domains, and he has been involved in the certification/qualification of run-time libraries and automatic code generators from modeling languages.

Throughout his career he has worked on the definition of language profiles for embedded systems, and the design and implementation of the run-time support required for executing on bare-metal targets.