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.
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:
64 bits architectures,
the evolution of the processor instruction set (for example SIMD instruction sets),
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,
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.
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
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
sedscript 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).
remove parent → children dependencies and especially when the parent has
begin .. endelaboration code,
begin .. endelaboration code.
analyze the elaboration table and make the necessary adjustments through, among other things, pragmas to complete the elaboration of the Ada Code.
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.
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,
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
removal of the warning with a
pragma warnings (Off,...)when it was considered unfounded.
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.
Initialize_Scalarsconfiguration pragma. This pragma in conjunction with the run-time option
Full-Validity-Checksmay 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.
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
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
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
Detect_Blocking configuration pragma.
Locking_Policy pragmas must be explicitly defined.
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.
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 (
native representation of data (mutable variant records for example),
use of attributes, pragmas or implementation-specific aspects,
use of compiler intrinsics,
use of implementation-related units,
use of inline machine code,
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.
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…).
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
option with optimizers enabled 1.
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 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.
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.
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.