• Aucun résultat trouvé

2.3 Un modèle orienté autour des variables

2.3.3 Découverte des contraintes à vérifier

2.3.3.2 L’interprétation abstraite

Une analyse statique exhaustive d’un programme doit permettre de prédire l’état du programme en tout point de son exécution et pour tous les scénarios d’exécution possibles. Cela revient à produire par le calcul l’ensemble des traces d’exécution possibles du programme. Les résultats de cette analyse permettent de vérifier que le programme respecte ou non une ou plusieurs contraintes données (par exemple, le programme ne doit jamais effectuer de division par zéro). Toutefois, réaliser une telle analyse pour un programme quelconque et dans un temps fini n’est pas possible dans le cas général.

En effet, il s’agit d’un problème indécidable (voir le théorème de Rice [Ric53] ainsi que le problème de l’arrêt [BM82]). Cependant, ce type d’analyse est pourtant nécessaire pour pouvoir connaître exactement le domaine de variation de chacune des variables manipulées par le programme jusqu’à un point donné de son exécution. Le calcul de ces domaines de variation souffre donc aussi de cette indécidabilité. Pour contourner ce problème, de nombreuses techniques ont été proposées parmi lesquelles l’interprétation abstraite [CC76, CC77a, CC77b, Cou02] est l’une des plus performantes et l’une des plus utilisées.

Cette approche repose sur une approximation de la sémantique du programme analysé. En effet, contrairement à la sémantique concrète qui décrit fidèlement l’exé-cution du programme, une sémantique abstraite ne raisonne plus en terme de valeurs mais en terme de propriétés (par exemple, suite à l’exécution d’une instruction,

plutôt que de déduire que x = 1, on se contente de savoir que x ≥ 0). Ce qui per-met de définir une sémantique abstraite, c’est l’ensemble des propriétés que celle-ci peut exhiber. C’est donc en limitant le nombre de propriétés que l’on s’assure de la calculabilité de cette sémantique. Cependant, cela implique également que les propriétés utilisées pour décrire les domaines de variation des variables sont des sur-approximations des propriétés concrètes. Les propriétés ainsi obtenues sont donc aussi dites abstraites.

Toute la difficulté de l’interprétation abstraite est de trouver le niveau d’abstrac-tion qui permet à la fois de résoudre le problème de calculabilité de la sémantique du programme et de fournir en résultat des propriétés suffisamment précises pour que la vérification du respect des contraintes données ait un sens. En fait, il faut diminuer la précision de l’analyse pour rendre celle-ci réalisable mais pas trop au risque de ne pas être capable de répondre au problème posé. Une fois que ce niveau d’abstraction est défini, le calcul de la sémantique d’un programme revient de nou-veau à calculer l’ensemble des traces d’exécution possibles du programme mais cette fois en raisonnant à l’aide des seules propriétés abstraites en lieu et place des pro-priétés réelles. Une fois l’analyse effectuée, les propro-priétés abstraites sont traduites en propriétés réelles (par exemple, x ≥ 0 devient x ∈ {0, ..., 127} si x est un entier codé sur 1 octet). Notons que cela implique que l’on doit être capable de traduire des propriétés concrètes en propriétés abstraites et réciproquement.

Un exemple simple d’abstraction pour les variables de type entier consiste à raisonner sur leur parité. Si nous limitons les opérations sur les entiers à petit nom-bre, par exemple à l’addition, à la soustraction, et à la multiplication, alors il est toujours possible de déterminer si le résultat est pair ou impair. Cette propriété abstraite, certes triviale et peu utile en pratique, permet d’illustrer sur un exemple l’interprétation abstraite.

En effet, durant le processus d’analyse, il est important de maintenir la correction de l’abstraction de chaque instruction du programme. Autrement dit, le processus d’abstraction doit préserver les propriétés de la sémantique concrète (par exemple, si une instruction change la parité d’une variable, cette propriété est conservée par la sémantique abstraite). Il est possible de garantir cette correction en utilisant une méthode formelle basée sur les fonctions monotones pour ensembles partiellement ordonnés et plus particulièrement les treillis [CC77a].

Plus précisément, définissons C comme étant l’ensemble des valeurs concrètes et P(C) comme l’ensemble des parties de C partiellement ordonnées par inclusion (⊆). Définissons également A comme l’ensemble des propriétés abstraites partiellement ordonnées par la relation ⊑. Soit γ la fonction qui représente chaque abstraction (un élément de A) par le sous-ensemble de valeurs concrètes correspondantes. Cette fonction correspond au processus de concrétisation. Elle permet d’obtenir pour un ensemble de valeurs abstraites données un ensemble de valeurs concrètes. Soit α la fonction réciproque qui représente chaque sous-ensemble de valeurs concrètes par l’abstraction qui lui correspond le mieux. Cette fonction correspond au processus

d’abstraction. Elle permet d’obtenir pour un ensemble de valeurs concrètes donné un ensemble de valeurs abstraites. C’est sur ces deux fonctions que repose la correction de l’interprétation abstraite.

Cependant, un processus d’analyse statique reposant sur l’interprétation ab-straite nécessite de procéder à des aller-retours entre les valeurs concrètes et les valeurs abstraites. Cela suppose que la composition de ces deux fonctions (γ ◦ α et α ◦ γ) doit satisfaire un certain nombre de propriétés. En fait, il a été montré que pour garantir la correction de l’abstraction, lorsque que α et γ sont des fonctions respectivement monotones sur P(C) et A, celles-ci doivent respecter les propriétés suivantes [CC77a] :

 ∀ S ∈ P(C) S ⊆ γ ◦ α(S)

∀ a ∈ A α ◦ γ(a) ⊑ a

La première propriété garantit que le processus d’abstraction est sûr dans le sens où il produit une sur-approximation. Plus précisément, elle garantit que l’abstraction d’un ensemble de valeurs concrètes donne un ensemble de valeurs abstraites qui cor-respond au mieux à l’ensemble de valeur concrètes contenant l’ensemble de valeurs concrètes de départ. Réciproquement, la seconde propriété garantit que le processus de concrétisation est sûr dans le sens où il produit une sous-approximation. Cette propriété est souvent limitée à une égalité stricte, à savoir α ◦ γ(a) = a, ce qui im-plique que toutes les propriétés abstraites sont exactes. Si nous reprenons l’exemple de la parité, nous pouvons définir A comme l’ensemble {⊥, pair, impair, ⊤} associé à la relation d’ordre partiel suivante :

pair

impair

Notons que A est un un treillis que γ est alors définie par : γ(pair) = {−∞, . . . , −4, −2, 0, 2, 4, . . . , +∞} γ(impair) = {−∞, . . . , −3, −1, 1, 3, . . . , +∞}

γ(⊤) = [−∞, +∞] γ(⊥) = ∅

Réciproquement, α est définie par : β(2n) = pair β(2n + 1) = impair

Appliquons maintenant ceci à un exemple simple. La figure 2.9 contient une suite d’instructions en langage C (ce sont les lignes numérotées). Le code de cette figure contient notamment une instruction de boucle (ligne 03) et une instruction de branchement conditionnel (ligne 04). L’objectif est de déterminer après analyse la parité des variables x et y. Grâce à l’interprétation abstraite, il est possible de déterminer que les variables x et y sont respectivement pair et impair à la fin de l’exécution du code. En effet, avant l’exécution de la boucle, les variables sont dans la situation inverse, à savoir respectivement impair et pair. Ni l’instruction exécutée systématiquement par la boucle (ligne 07) ni l’instruction exécutée dans la branche conditionnelle ne changent la parité de ces variables. Ceci arrive après l’exécution de la boucle, par les instructions ligne 09 et 10.

00. int x = 0; //@ ∃(k) ∈ N, x = 2k 01. int y = x; //@ ∃(j, k) ∈ N2, x= 2j ∧ y = 2k 02. x = x + 1; //@ ∃(j, k) ∈ N2, x= 2j + 1 ∧ y = 2k 03. while (x > 8){ 04. if (f(x,y)){ //@ ∃(j, k) ∈ N2, x= 2j + 1 ∧ y = 2k 05. y = y + 2; //@ ∃(j, k) ∈ N2, x= 2j + 1 ∧ y = 2k 06. } //@ ∃(j, k) ∈ N2, x= 2j + 1 ∧ y = 2k 07. x = x + 2; //@ ∃(j, k) ∈ N2, x= 2j + 1 ∧ y = 2k 08. } //@ ∃(j, k) ∈ N2, x= 2j + 1 ∧ y = 2k 09. x = x + 1; //@ ∃(j, k) ∈ N2, x= 2j ∧ y = 2k 10. y = y + 1; //@ ∃(j, k) ∈ N2, x= 2j ∧ y = 2k + 1

Figure2.9 – Exemple d’interprétation abstraite

L’utilisation de l’interprétation abstraite permet donc d’analyser un programme à partir d’une sur-approximation de sa sémantique concrète. Cette sur-approximation étant garantie, toutes les propriétés vérifiées par la sémantique abstraite le sont aussi par la sémantique concrète. En conséquence, ces propriétés calculées par interpréta-tion abstraite peuvent alors être utilisées pour démontrer (ou non) qu’un programme respecte des propriétés concrètes données. L’exemple de la parité est trivial, en pra-tique on s’intéresse plutôt notamment à l’inférence de type ou encore à l’analyse d’intervalle.