• Aucun résultat trouvé

Définition du langage L 0

Dans le document Analyse des pointeurs pour le langage C (Page 85-89)

Tous les sous-langages, L0, L1, L2, sont définis par des sous-ensembles de ces règles. Ce

n’est pas qu’au chapitre 6 que le langage C est analysé dans sa totalité.

4.2

Définition du langage L

0

Compte tenu de la richesse du langage C et de la complexité de l’analyse des pointeurs, nous avons choisi de commencer par définir l’analyse pour un sous-ensemble du langage C, appelé L0. Le sous-langage contient les principaux opérateurs impliquant des pointeurs comme la prise

d’adresse « & » et le déréférencement « * ». L’opérateur p->x peut être réécrit sous la forme (*p).x.

4.2.1

Définition des domaines

Nous commençons par définir les différents domaines des éléments qui interviennent dans notre sous-ensemble du langage Ll0. Les domaines dont nous avons besoin sont essentielle-

ment le domaine des expressions E, le domaine des chemins d’accès mémoire constants A ainsi que le domaine des statements S. Nous définissons tout d’abord à la table 4.1 les domaines simples nécessaires à la construction des domaines plus complexes. La constante « ∗ », à ne pas confondre avec le déréferencement de pointeur, désigne n’importe quelle valeur entière. Quand elle est utilisée comme indice, l’ensemble des valeurs correspondantes est restreint à l’intervalle [0, N [ où N est défini par la déclaration du tableau utilisé.

Domaine Définition

Booléen B = {true, false}

Constante C = N ∪ {NULL, undefined, ∗} Identificateur I

Tableau 4.1 – Domaines simples

4.2.1.1 Définition du domaine T des types

Les variables et les expressions des programmes que nous traitons sont typées. Comme nous traitons un sous-ensemble du langage C nous avons inclus un sous-ensemble des types en ne gardant parmi les types basiques que le type int. Quant aux types agrégés, les constructeurs de types tableaux et structures ont été inclus. Bien sûr le domaine des types comporte aussi le type pointeur.

t ∈ T ::= int|struct|pointer(t0)|array(t0,cte)|overloaded (4.1) avec cte ∈ C, t0 ∈ T et struct une fonction des identificateurs (un nom de champ) vers les types

(le type du champ) struct : I → T . Le type overloaded représente le sommet du treillis type introduit à la sous-section 5.3.4.

4.2.1.2 Définition des éléments de l’ensemble E des chemins d’accès non constants

Nous commençons par définir le domaine E des chemins d’accès non constants. Un chemin d’accès est construit à partir d’une expression qui est l’élément le plus fondamental du langage C. Il est dit non constant dès qu’il implique un déréférencement mémoire, même si la case mémoire utilisée pour le déréférencement est constante sur la portée considérée. C’est une définition syn- taxique. Le langage comporte les accès aux champs des structures, aux éléments des tableaux

60 Chapitre 4. L’analyse intraprocédurale simplifiée

ou encore l’accès à la case mémoire pointée par un pointeur. Mathématiquement, il n’est pas possible de distinguer entre un élément de E et un élément de A (langage défini dans la section suivante 4.2.1.3). C’est l’information de type associée implicitement à chaque chemin ou sous- chemin qui permet de distinguer entre déréférencements d’une part, et indexations ou accès à un champ d’autre part.

Au niveau de la section 4.6, nous verrons qu’un lhs2peut être une expression complexe avec

de possibles effets de bords. Les expressions complexes et développées sont définies au niveau de la sous-section 4.6. Dans cette section, nous nous contentons d’expressions simples, sans effets de bords.

e ∈ E ::= Idt|e1.ft| ∗ et|&e1|e1[e2]t|ctet (4.2)

avec t ∈ T , cte ∈ C, Id ∈ I, f ∈ I, e ∈ E.

4.2.1.3 Définition des éléments de l’ensemble des chemins constants A

Nous commençons par définir l’ensemble des chemins constants A qui va permettre de tra- duire les déréférencements de pointeurs sans passer par un code à 3-adresses. En effet, les expressions gauches, les lhs, sont traduites en chemins d’accès dans E en généralisant la notion d’indexation aux champs de structures et aux déréférencement (voir le programme 4.1).

s1 -> s1 s1.val -> s1[val] a[1] -> a[1] *p -> p[0] (*q).next -> q[0][next]

Prog 4.1 – Traduction des accès mémoire en chemins constants

C’est la base du treillis, aussi appelé domaine abstrait, que nous allons utiliser pour abs- traire les relations points-to. L’ensemble A est construit à partir de l’ensemble I des variables d’une fonction et de l’opérateur d’indexation qui sert à représenter les accès aux champs des structures tout comme les accès aux éléments de tableaux. Par exemple, si a est déclarée int a[10], l’expression a[2] est un élément de A. Dans A, les arguments de l’opérateur d’indexation sont toujours des constantes. Une adresse au sens de A est donc un lhs constant, ou, dans la terminologie interne, un chemin d’accès constant. Le chemin constant correspond à une adresse mémoire unique dans le cas d’un scalaire, ou à un ensemble d’adresses mémoire contigües dans le cas d’une structure ou d’un tableau. Le chemin est valable dans toute la portée (scope) du programme où sa variable initiale est définie.

Par définition, les chemins constants sont inclus dans les expressions, A ⊂ E. Ils représentent des expressions qui ont été évaluées et où chaque déréférencement de pointeur a été remplacé par l’emplacement mémoire pointé. Avec l’introduction de l’information sur les types, les éléments de A correspondent à des chemins constants et typés. Le domaine A est défini récursivement comme suit :

a ∈ A ::= Idt|ctet|(at.c)t0|at[ctei] (4.3)

avec t ∈ T , c ∈ I, cte ∈ C. Donc, un chemin constant est soit un identificateur, soit une constante qui peut être le pointeur nul, NULL, un indice quelconque, ∗, ou encore un pointeur indéfini, undefined, soit encore un chemin postfixé par un champ ou par un indice constant de type entier.

4.2. Définition du langage L0 61

4.2.1.4 Définition du domaine P des programmes

Le domaine des programmes correspond à la notion de fonction en C, avec des paramètres formels en entrée ainsi qu’une liste de variables locales ; la valeur de retour est ignoré pour le mo- ment. En plus des déclarations, la fonction comporte un ensemble d’instructions regroupées en un statement3. Nous commençons par définir les sous-domaines qui constituent le domaine P.

Définition du domaine D des déclarations Le domaine des déclarations est défini comme une fonction des identificateurs (les noms des variables) vers les types.

D = I → T (4.4)

Les déclarations, notées par la suite δ, concernent les paramètres d’une fonction ou les variables locales. La fonction de déclaration doit être définie pour toutes les variables d’un programme.

Définition du domaine S des statements Le domaine des « statements » est défini comme suit :

s ∈ S ::=Sequence(s1, s2, ...)|Test(eB, s1, s2)|Assign(e1, e2) (4.5)

avec e ∈ E. Un statement peut être une séquence de statements , ou bien une structure de contrôle de type test où une condition est évaluée. Si la condition eB de type B est évaluée à

vrai, c’est le statement s1qui est exécuté, sinon c’est le statement s2. Comme c’est l’analyse des

pointeurs que nous ciblons, le domaine S inclut aussi le statement d’affectation qui prend comme arguments deux expressions de types compatibles4. Enfin, comme expliqué dans l’introduction

de ce chapitre, il n’y a pas de boucles dans le langage L0. Les calculs de points fixes sont traités

dans le chapitre 5.

Définition du domaine P des programmes Après la définition des domaines D des déclara- tions et des statements S, nous pouvons à présent définir le domaine des programmes P.

P : D × D × S p ∈ P ::= Prog(δp, δl, s)

où δp représente les déclarations des paramètres formels et δl les déclarations des variables

locales au niveau du corps de la fonction. Les domaines Dδp et Dδl de définition des fonctions

δpet δlsont nécessairement disjoints, un identificateur ne pouvant pas être en même temps un

paramètre et une variable locale.

Dδp∩ Dδl= ∅

De manière plus générale, on utilisera éventuellement δ la fonction de déclaration qui est l’union des deux fonctions précédentes :

δ(Id) =si Id ∈ Dδpalors δp(Id)sinon δl(Id)

4.2.2

Syntaxe du langage L

0

En conclusion nous reprenons la syntaxe du langage C, donnée précédemment par la fi- gure 4.1, pour mettre en gras les instructions du langage L0.

3. Un bloc de base d’instructions.

4. Pour alléger les notations, les informations t de type dans et ne sont données qu’en cas de besoin. On définit

62 Chapitre 4. L’analyse intraprocédurale simplifiée

<statement > S : := <expression > = < expression > | if < expression > < s1 > else < s2 > | sequence < s >∗ <expression > E : := <constant > | < reference > | (< expression >) | < u_op > < expression >

| < expression > < b_op > < expression > <unary_op > u_op : := | &| *

<binary_op > b_op : := =

<reference > R : := <name > | < name >[< expression >∗] <type > T : := int | float | | pointer(t) | struct | overloded

FIGURE4.2 – Syntaxe abstraite de l’ensemble du langage L0

4.2.3

Typage des expressions

La détermination des types des expressions est la première étape de l’analyse de pointeurs. En effet, l’analyse s’effectue principalement sur les affectations dont la partie gauche est un pointeur. Il est donc nécessaire de développer une fonction qui renvoie le type de l’expression passée en argument.

4.2.3.1 Règles du typage

Les règles strictes de typage sont détaillées par la suite, en particulier en ce qui concerne la cohérence des types entre les pointeurs et les emplacements mémoire vers lesquels ils pointent. D’autres règles sont mentionnées mais elles sont supposées être vérifiées par la phase de véri- fication des types du compilateur (« type-checker »), comme par exemple le fait que l’index d’un élément de tableau doit être un entier. Comme le but est d’analyser un sous-ensemble du langage C, nous commençons par introduire l’équivalence des types entre les types pointeur et pointeur sur tableau. Ceci permet de savoir si deux types sont compatibles entre eux ou non, au delà de l’égalité syntaxique :

pointer(t) ∼ pointer(array(t, cte)) type_eq(t1, t2) ⇔ t1= t2∨ t1∼ t2

Cette équivalence est utilisée par la suite pour tester la cohérence des arcs points-to.

4.2.3.2 Détermination du type d’une expression

Une des étapes de l’analyse des pointeurs est d’identifier qu’en partie gauche d’une assi- gnation apparaît un pointeur. Comme les expressions peuvent vite devenir complexes, il faut concevoir une fonction qui itère sur la partie gauche jusqu’à déterminer son type final. La fonc- tion qui détermine le type est appelée type_of. Elle prend en argument une expression typée ou non. La fonction est une disjonction sur les différents cas d’expressions. Elle renvoie le type des éléments de E. Au préalable nous avons besoin de définir la fonction γ qui renvoie le type associé à une constante ; sa signature est la suivante :

γ : C → T cte 7→ t

Dans le document Analyse des pointeurs pour le langage C (Page 85-89)