Démarche de portage d’un « legacy » Ada

Ada Lovelace

Le portage d’un « legacy » Ada peut s’avérer être une tâche délicate malgré les qualités intrinsèques du langage comme, par exemple, sa réelle portabilité. Cet article identifie les principaux problèmes pouvant être rencontrés et la façon dont ils peuvent être au mieux adressés.

Cet article concerne le langage Ada mais, en termes de démarche, certains points sont transposables à d’autres langages.

Il est important de noter qu’une partie des solutions proposées s’appuient sur la technologie GNAT et sont principalement basées sur le retour d’expérience de Systerel dans ce domaine.

Introduction

Un portage est généralement motivé par le besoin de traiter une obsolescence matérielle et/ou de l’exécutif. Il est généralement isofonctionnel et avec des performances attendues identiques ou supérieures. Celui-ci se fait généralement après un temps relativement long pouvant varier entre 10 et 30 ans. Il est donc probable que les technologies devant être mises en œuvre n’existaient pas à l’époque. On peut par exemple citer :

  • les architectures SMP,

  • les architectures x64 bits,

  • l’évolution du jeu d’instruction du processeur (par exemple les jeux d’instructions SIMD),

  • etc.

À cela viennent souvent se greffer :

  • un changement du compilateur et de la version du langage Ada supportée,

  • un changement d’« endianness » du processeur,

  • un changement de matériel,

  • un changement d’exécutif ou de RTOS,

  • etc.

On utilisera dans la suite les adjectifs source (e.g. compilateur source, architecture source) pour ce qui concerne l’environnement du logiciel « legacy » et cible ce qui concerne l’environnement du logiciel porté.

Prérequis au portage

La liste des prérequis peut varier suivant les projets mais le plus important est le fait d’être capable de régénérer à l’identique l’application d’origine. Cela garantit en effet que les sources prises en compte sont les bonnes car après plusieurs années, il n’est pas toujours simple de maîtriser la version des entrants…

Si l’image à reconstruire a un format binaire de haut niveau (exemple ELF) il ne sera probablement pas possible de comparer directement les binaires obtenus. Pour ce faire, il sera nécessaire de les normaliser de part et d’autre en utilisant par exemple la commande strip.

Principaux risques inhérents au portage d’un « legacy » Ada

Impossibilité d’élaborer le code Ada

Ce risque est probable si :

  • le code devant être porté est mal architecturé,

  • le code contient beaucoup de pragmas contrôlant l’ordre d’élaboration, contraignant ainsi le binder dans ses choix. Il est alors tout-à-fait possible que le binder cible ne puisse pas trouver un ordre d’élaboration compatible avec l’ensemble des dépendances ainsi introduites dans le programme (même s’il en existe un!),

  • le binder cible est moins performant que le binder source.

Le traitement de ce risque nécessite une bonne connaissance du mécanisme d’élaboration Ada ainsi que de la manière dont celui-ci est implémenté au sein des binders sources et cibles. Par la suite nous considèrerons le binder cible comme étant celui du GNAT.

Le modèle d’élaboration par défaut du GNAT est le modèle statique 1. Ce modèle fiable a entre autres pour avantage de mettre en exergue des problèmes d’architecture de l’application. Hélas, la structuration du « legacy » devant être porté peut ne pas permettre d’appliquer ce modèle sans nécessiter un refactoring profond.

En conséquence, si le modèle dynamique est mis en œuvre (et donc bien souvent reconduit) et si le problème d’élaboration est avéré, alors les étapes suivantes doivent être réalisées :

  • Supprimer tous les pragmas liés à l’élaboration (un script sed peut effectuer simplement cette tâche).

  • Faire en sorte que toutes les clauses de contextes soient utilisées et placées au niveau adéquat (les avertissements du compilateur seront d’une aide précieuse).

  • Dans la mesure du possible :

    • enlever les dépendances père → fils et plus particulièrement quand le père comporte du code d’élaboration begin .. end,

    • enlever le code d’élaboration begin .. end non justifié.

    • analyser la table d’élaboration et faire les ajustements nécessaires par, entre autres, le biais de pragmas permettant de mener à son terme l’élaboration du code Ada.

Note

La méthode du « Callback » peut permettre de casser des dépendances même si celles-ci sont nécessaires.

En Ada 2012, l’utilisation des « Expression Functions » peut aider à résoudre certains problèmes d’élaboration.

Adhérences à l’implémentation du compilateur source et/ou permissivité du compilateur source

Un code fonctionnant avec le compilateur source peut ne pas fonctionner avec le compilateur cible si le code met en œuvre des traits spécifiques à l’implémentation du compilateur source. L’exemple classique est de dépendre du mode de passage des paramètres dans l’implémentation (ici passage par copie ou par référence). Dans ce cas, le code est considéré comme erroné et peut conduire à des erreurs bornées ou non bornées.

Concernant la permissivité du compilateur, le compilateur source peut ne pas émettre d’avertissements là où manifestement il le devrait. Par exemple, si celui-ci ignore (liste non exhaustive) :

  • l’utilisation (lecture) d’une variable non initialisée,

  • le fait qu’un paramètre out d’un sous-programme n’est pas assigné,

  • que la source et la cible d’une conversion sans vérification soient de tailles différentes,

  • qu’il y ait des recouvrements (overlays) mémoire,

  • etc.

Dans ces deux cas, il est probable qu’une fois porté, le code ne s’exécutera plus correctement.

La démarche proposée est alors la suivante :

  • Toutes les conversions sans vérifications (i.e. Ada.Unchecked_Conversion) jugées incorrectes par le compilateur cible (avertissements du compilateur) doivent être traitées par une des actions suivantes :

    • ajout des clauses de représentation adéquates,

    • suppression de l’utilisation de la fonction générique Ada.Unchecked_Conversion,

    • suppression de l’avertissement avec un pragma Warnings (Off,...) quand celui-ci était considéré comme injustifié.

      Avis

      Soyez extrêmement prudent sur la suppression des avertissements qui ne peut être faite qu’avec une bonne connaissance du compilateur et de l’annexe M du Manuel de référence Ada. Documenter les raisons de la suppression de l’avertissement.

    • tous les avertissements de la front-end du compilateur GNAT sont activés et par exemple les avertissements suivants (exprimés sous forme d’expressions, liste non exhaustive) sont considérés comme des erreurs et doivent donc être corrigés.

      pragma Warning_As_Error ("*be raised at run time*"); -- equivalent à -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*");
      -- …
      
  • Avec l’outil GNATCheck vérifier au minimum le respect de la règle Unassigned_OUT_Parameters. Cette règle vérifie qu’un paramètre out d’un sous-programme est toujours assigné. Afin d’être certain que cette règle n’est jamais violée, une deuxième passe avec l’outil AdaControl peut s’avérer utile. Dans ce cas, il faut vérifier la règle équivalente check improper_initialization (out_parameter). Corriger le code en conséquence quand la règle s’avère être violée.

  • Appliquer le pragma de configuration Initialize_Scalars. Ce pragma en conjonction avec l’option de la run-time Full-Validity-Checks (-gnatVa) permet d’éventuellement détecter, à l’exécution, les données non valides ou non initialisées. Lancer des tests représentatifs de l’application et, en fonction des résultats, modifier le code en conséquence.

Note

La compilation avec l’option -O3 (optimisations agressives du temps d’exécution) permet d’exercer de façon plus importante la back-end du compilateur gcc. Cela permet de mettre en exergue des avertissements qui peuvent ne pas apparaitre en -O0.

Note

Pour corriger les problèmes liés à la non initialisation de variables, il peut être intéressant d’utiliser les facilités offertes par le langage telles que les agrégats par défaut <> ou les aspects Default_Value et Default_Component_Value.

Modification du comportement temps réel

Dans le cadre d’applications contenant des traits concurrents, si une partie de l’ordonnancement est basée sur :

  • les priorités des tâches ou des objets protégés,

  • des attentes actives ou l’utilisation d’instructions de type delay ou delay until,

alors il est possible que le comportement temps réel ne soit plus le même. Cela est d’autant plus vrai si les caractéristiques des processeurs source et cible sont notablement différentes (par exemple fréquence du processeur et mise en œuvre d’une architecture SMP).

Ce risque ne peut être réellement levé qu’en phase d’intégration et de validation. Cependant, dans le cadre du portage, la run-time Ada doit être configurée pour détecter des situations de blocage à l’exécution (dead-lock) au travers du pragma de configuration Detect_Blocking. Les pragmas Task_Dispatching_Policy, Queuing_Policy et Locking_Policy doivent être explicitement définis.

Introduction des architectures parallèles (SMP)

Comme pour le risque précédent, si l’application portée contient des traits concurrents qui n’étaient pas correctement traités sur une architecture non SMP, alors des problèmes peuvent être mis en exergue.

Si le risque est avéré, alors une solution simple est de forcer les affinités des threads afin de se ramener à une architecture mono-cœur du point de vue des processus concernés. Cela n’empêche cependant pas de bénéficier de l’architecture multi-cœur vis-à-vis des autres applications qui pourraient être mises en œuvre. Forcer ainsi les affinités permet aussi d’introduire un minimum de déterminisme.

Dépendance du code à l’annexe M ou aux traits d’implémentations non documentés

À titre d’exemple, si le code est dépendant aux traits suivants, alors le portage sera complexifié et donc plus risqué (liste non exhaustive) :

  • taille des piles (Storage_Sizes),

  • représentation native des données (exemple types mutants),

  • sérialisation des données,

  • utilisation d’attributs, de pragmas ou d’aspects propres à l’implémentation,

  • utilisation d’intrinsics du compilateur,

  • utilisation d’unités liées à l’implémentation,

  • utilisation du code machine en ligne,

  • etc.

Si le risque est avéré, alors il n’y a pas une démarche particulière à appliquer. Il faut simplement bien intégrer ces points et les prendre en considération lors du portage.

Version du langage Ada

Si le portage met en œuvre une version différente et généralement supérieure du langage Ada, alors des incompatibilités peuvent apparaître. Par exemple, un identificateur devenant un mot réservé (exemple interface pour Ada 2005).

Le risque est simplement levé à l’issue de la compilation de l’ensemble du code.

Dans le cas où il serait nécessaire de développer du code, il est important de noter que le passage à Ada 2012 permet d’avoir un code à la fois plus concis et plus clair (utilisation de la notion d’aspects, de nouvelles formes d’expressions, …).

Optimisations du compilateur cible

Des optimisations liées au compilateur cible peuvent rendre un code inopérant. Par exemple le compilateur peut juger qu’une écriture dans une variable n’est pas nécessaire car celle-ci est remise à jour quelques instructions plus loin. Cela peut être préjudiciable si cette variable a une implantation mémoire spécifique. Les problématiques d’aliasing sont traitées dans le risque suivant.

Ici, un choix raisonnable est de ne pas activer les optimisations (i.e -O0). Ce choix est d’autant plus justifié que généralement l’architecture cible est très largement supérieure en terme de performances à l’architecture source ; l’activation des optimisations n’est donc probablement pas requise.

Il est fortement recommandé de ne pas utiliser l’option --gnatVa précédemment citée avec les optimiseurs activés 1.

Respect des règles d’aliasing

Le typage fort du langage Ada permet au compilateur de faire des hypothèses fortes quant au non aliasing d’objets, lui permettant ainsi de générer un code efficace. Cela étant, l’utilisation par exemple de conversions sans vérifications ou d’overlay d’adresses peut amener à des optimisations inopinées du fait d’un aliasing caché. Les hypothèses faites par le compilateur peuvent s’avérer fausses et donc produire un code erroné.

La meilleure approche est de désactiver toute forme d’optimisation vis-à-vis de l’aliasing en utilisant l’option -fno-strict-aliasing et le pragma de configuration No_Strict_Aliasing12.

Transposition des options du compilateur source

Le code peut s’appuyer sur des options ou des fonctionnalités du compilateur qui ne sont pas transposables avec le compilateur cible (exemple : le préprocesseur).

Ici, il n’y a pas non plus une démarche particulière à appliquer, il faut simplement prendre en compte les contraintes technologiques.

Passage à une architecture 64 bits

Lors du portage, la mise en œuvre d’une architecture SMP peut aussi s’accompagner d’un passage d’une architecture 32 bits à 64 bits. Des adhérences à ces traits peuvent complexifier le portage.

Ici, il n’y a pas non plus une démarche particulière à appliquer, il faut simplement prendre en compte les contraintes de l’architecture cible. Si des modifications sont faites, il faut faire en sorte que le code porté gomme toute adhérence à l’architecture cible, sachant que le langage Ada offre pour cela toutes les fonctionnalités requises.

Changement d’endianness

Celui-ci est adressé simplement par le biais des pragmas (ou aspects) Bit_Order2 et Scalar_Storage_Order2.

Attention!

L’utilisation de ces pragmas peut dégrader significativement les performances si un minimum de précautions ne sont pas prises.

Conclusion

Le langage Ada et la technologie GNAT apportent des solutions efficaces et simples afin de garantir que le portage d’un legagy Ada soit fait dans les meilleures conditions.

Il est aussi important de noter que les solutions proposées peuvent aussi être mises en œuvre dans le cadre d’un nouveau développement. Ainsi donc, elles contribueront de fait à la qualité de celui-ci et mettront aussi en place les bases nécessaires au bon déroulement d’un futur portage.

Commentaires