Porting an Ada legacy software

Ada Lovelace

Porting an Ada legacy software can be a delicate task despite the intrinsic qualities of the Ada language such as, for example, its true portability. This article identifies the main problems that may be encountered and how they can best be addressed.

This article is about the Ada language but, in terms of approach, some points are transposable to other languages.

It is important to note that some proposed solutions are based on GNAT technology and are mainly based on Systerel’s feedback in this field.

Introduction

A software port is usually motivated by the need to address material and/or OS obsolescence. It is generally isofunctional and with the same or higher expected performance. This is usually done after a relatively long time that can vary between 10 and 30 years. Therefore, it is likely that the technologies that need to be implemented did not exist at the time, for example:

  • SMP architectures,

  • 64 bits architectures,

  • the evolution of the processor instruction set (for example SIMD instruction sets),

  • etc.

This is often combined with:

  • a change in the compiler and in the supported Ada language version,

  • a change in processor endianness,

  • a change in hardware,

  • a change of kernel or RTOS,

  • etc.

In the following, we will refer as source (e.g. source compiler, source OS) for the environment of the legacy software and target for the environment of the ported software.

Portage Prerequisites

The list of prerequisites may vary from project to project, but the most important thing is to be able to regenerate the original application identically. This indeed guarantees that the source files taken into account are the right ones because after several years, it is not always easy to have the version of the inputs under control.

If the image to be reconstructed has a high level binary format (e.g. ELF) it will probably not be possible to directly compare the obtained binaries. To do so, it will be necessary to standardize them from on both sides using for example the command strip.

Main risks inherent to porting a legacy Ada software

Impossibility to elaborate the Ada code

This risk is likely if:

  • the code to be ported is poorly architected,

  • the code contains many pragmas controlling the order of elaboration, thus constraining the binder in his choices. It is quite possible that the target binder cannot find a processing order compatible with all the dependencies introduced in the program (even if one exists!),

  • the target binder is less efficient than the source binder.

Tackling this risk requires a good knowledge of the Ada elaboration mechanism and how it is implemented within the source and target binders. In the following we will assume the target binder to be the GNAT binder.

The default elaboration model for GNAT is the static model 1. One of the advantages of this reliable model is that it pinpoints application architecture problems. Unfortunately, the structure of the legacy software to be ported may not allow to apply this model without requiring a deep refactoring .

Therefore, if the dynamic model is used (and therefore often used again when porting) and the elaborating problem is proven, then the following steps should be carried out:

  • Delete all pragmas related to the elaboration (a sed script can simply perform this task).

  • Make sure that all context clauses are used and placed at the right level (compiler warnings will be of great help).

  • Wherever possible:

    • remove parent → children dependencies and especially when the parent has begin .. end elaboration code,

    • remove unwarranted begin .. end elaboration code.

    • analyze the elaboration table and make the necessary adjustments through, among other things, pragmas to complete the elaboration of the Ada Code.

Note

The “Callback” method may allow getting get rid of some dependencies even if they are necessary.

In Ada 2012, the use of “Expression Functions” can help solve some elaboration problems.

Adherence to the source compiler implementation and/or permissiveness of the source compiler

Code that works with the source compiler may not work with the target compiler if the code uses features specific to the implementation of the source compiler. The classic example is when the implementation depends on the way the parameters are passed (i.e. passing by copy or by reference). In this case, the code is considered erroneous and can lead to bounded or unbounded errors.

Regarding the permissiveness of the compiler, the source compiler may not issue warnings where it obviously should. For example, if the source compiler ignores (non-exhaustive list):

  • the use (reading) of an uninitialized variable,

  • the fact that an out parameter of a subprogram is not assigned,

  • that the source and target of an unchecked conversion are of different sizes,

  • that there are memory overlays,

  • etc.

In both cases, it is likely that once ported, the code will no longer execute correctly.

The proposed approach is then as follows:

  • All unchecked conversions (i.e. Ada.Unchecked_Conversion) found to be incorrect by the target compiler (warnings) must be handled by one of the following actions:

    • addition of appropriate representation clauses,

    • removal of the use of the generic function Ada.Unchecked_Conversion,

    • removal of the warning with a pragma warnings (Off,...) when it was considered unfounded.

      Warning

      Be very cautious when removing warnings! Those removals can only safely be done with a very good understanding of the compiler and the annex M of the Ada Reference Manual. Add some comments to explain the rational of the removal.

    • all GNAT compiler front-end warnings are enabled and for example the following warnings (expressed as expressions, non-exhaustive list) are considered errors and should be fixed.

      pragma Warning_As_Error ("*be raised at run time*"); -- equivalent to -gnatwE
      pragma Warning_As_Error ("*types for unchecked conversion have different sizes*");
      pragma Warning_As_Error ("*referenced before it has a value*");
      pragma Warning_As_Error ("*uninitialized*");
      pragma Warning_As_Error ("*not set before return*");
      pragma Warning_As_Error ("*overlays*");
      pragma Warning_As_Error ("*program execution may be erroneous*");
      pragma Warning_As_Error ("*formal parameter * is not modified*");
      -- …
      
  • With the GNATCheck tool, check at least the rule Unassigned_OUT_Parameters is followed. This rule checks that an out parameter of a subprogram is always assigned. To ensure that this rule is never violated, a second pass with the AdaControl tool can be useful. In this case, check the equivalent rule check improper_initialization (out_parameter). Correct the code accordingly when the rule turns out to be broken.

  • Apply the Initialize_Scalars configuration pragma. This pragma in conjunction with the run-time option Full-Validity-Checks may enable the detection, at runtime, of invalid or uninitialized data. Run tests representative of the application and, depending on the results, modify the code accordingly.

Note

Compiling with the -O3 option (aggressive runtime optimizations) enables to exercise the back-end of the gcc compiler to a greater extent. This make it possible to emphasize warnings that may not appear in -O0.

Note

To correct problems related to the non initialization of variables, it can be interesting to use the features offered by the language such as the default aggregates <> or the Default_Value and Default_Component_Value aspects.

Modification of real-time behavior

In applications with concurrent features, if part of the scheduling is based on:

  • priorities of protected tasks or objects,

  • active waiting or the use of delay or delay until instructions,

then it’s possible that the real-time behavior is no longer the same. This is all the more true if the source and target processors are significantly different (for example processor frequency and implementation of an SMP architecture).

This risk can only really be removed during the integration and validation. However, as part of the porting, the Ada run-time must be configured to detect dead-lock situations at runtime through the Detect_Blocking configuration pragma. The Task_Dispatching_Policy, Queuing_Policy and Locking_Policy pragmas must be explicitly defined.

Introduction of parallel architectures (SMP)

As with the previous risk, if the application being ported contains concurrent traits that were not properly addressed on a non SMP architecture, then problems can be highlighted.

If the risk is proven, then a simple solution is to force the affinities of threads in order to return to a single-core architecture from the point of view of the processes concerned. However, this does not prevent other applications that may be run from benefiting of the multi-core hardware. Forcing affinities in this way also makes it possible to introduce a minimum of determinism.

Dependence of code on Appendix M or undocumented implementation traits

As an example, if the code is dependent on the following features, then the porting will be more complex and therefore riskier (non-exhaustive list):

  • stack size (Storage_Sizes),

  • native representation of data (mutable variant records for example),

  • data serialization,

  • use of attributes, pragmas or implementation-specific aspects,

  • use of compiler intrinsics,

  • use of implementation-related units,

  • use of inline machine code,

  • etc.

If the risk is proven, then there is no particular approach to apply. It is simply a matter of understanding these points and taking them into consideration when porting.

Ada language version

If the port uses a different and generally superior version of the Ada language, then incompatibilities may occur. For example, an identifier that becomes a reserved word (example interface for Ada 2005).

The risk is simply lifted after the compilation of all the code.

Should it be necessary to develop code, it is important to note that the transition to Ada 2012 allows for a code that is both more concise and clearer (use of the notion of aspects, new forms of expressions, etc…).

Target compiler optimizations

Optimizations related to the target compiler can render a code inoperative. For example, the compiler may judge that writing to a variable is not necessary because the variable is updated a few instructions later. This can be detrimental if this variable has a specific memory location. Aliasing issues are dealt with in the next section.

Here, a reasonable choice is not to activate the optimizers. (i.e -O0). This decision is all the more justified as the target architecture is generally much better in terms of performance than the source architecture; so activation of the optimizers is probably not required.

It is strongly recommended not to use the aforementioned --gnatVa option with optimizers enabled 1.

Adherence to the rules of aliasing

The strong typing of the Ada language allows the compiler to make strong assumptions about the non aliasing of objects, allowing it to generate efficient code. However, the use of, for example, unchecked conversions or overlay of addresses can lead to unexpected optimizations due to hidden aliasing. Assumptions made by the compiler may turn out to be wrong and thus produce erroneous code.

The best approach is to disable any form of aliasing optimization using -fno-strict-aliasing option and the No_Strict_Aliasing configuration pragma 1 2.

Transposing source compiler options

The code may rely on options or features of the compiler that are not transposable on the target compiler (example: the preprocessor).

Here, too, there is no particular approach to be applied, it is simply a matter of taking technological constraints into account.

Transition to 64-bit architecture

When porting, the implementation of an SMP architecture can also be accompanied by a switch from 32-bit to 64-bit architecture. Adherence to these traits can make porting more complex.

Here too, there is no particular approach to apply, you simply have to take into account the constraints of the target architecture. If modifications are made, it is necessary to make sure that the ported code erases any adherence to the target architecture, knowing that the Ada language offers all the required functionalities for this.

Change of endianness

This one is simply take into account via the pragmas or aspects: Bit_Order 2 and Scalar_Storage_Order 2.

Warning

The use of these pragmas can significantly degrade performance if a minimum of precautions are not taken into account.

Conclusion

The Ada language and GNAT technology provide efficient and simple solutions to ensure that porting an Ada legacy code is done with the best conditions.

It is also important to note that the proposed solutions can also be implemented as part of a new development. In this way, they will in fact contribute to the quality of the produced code and also lay the foundations for the smooth running of a future port.

Comments