LIF


Université de Provence

Centre de Mathématiques et d'Informatique


CNRS – Université de Provence – Université de la Méditerranée



Vérification de code mobile ou embarqué
19 juin 2004


E. Bremont, M. Daoudou, S. Ravelomanana

Sous la direction de Solange Coupet-Grimal

CMI, 39 Rue F. Joliot-Curie, 13453 Marseille France
{ebremont, mdaoudou, sravelom}@capucine.univ-mrs.fr




Résumé. Dans le monde d'aujourd'hui, de plus en plus d'applications s'échangent du code pour leur fonctionnement. Ce code, généralement échangé sous forme pré-compilé (bytecode) peut être affecté, soit accidentellement (erreur de programmation ou de transmission), soit intentionnellement (hackers,...). Le but de ce travail est de procéder à deux types de vérifications : une vérification de type et une vérification sur la forme des expressions manipulées, ce qui permettra, à terme et sous certaines conditions, de donner une borne sur la taille mémoire et le temps d'exécution nécessaire au code.

Mots clés. sécurité, vérification de bytecode, algorithme de Kildall, ...

Note. Vous pouvez télécharger une version pdf de ce rapport.

 Table des matières

 1  Introduction

Des transactions bancaires aux téléphones cellulaires en passant par les jeux sur consoles mobiles, de plus en plus d'applications requièrent l'utilisation de code embarqué. Ce code, échangé généralement sous une forme pré-compilée (bytecode), peut être naturellement le produit de la compilation d'un programme source écrit dans un langage donné (cartes à puce, ...), mais peut aussi être d'une provenance inconnue (jeux en réseaux, ...). Si dans le premier cas, on ne doute pas (ou peu) de sa correction, et ce, du fait - on peut le supposer - qu'il a été généré par un compilateur correct, il n'est pas garanti, en revanche, dans le deuxième cas que, la main l'ayant produit ne soit pas malicieuse ou du moins qu'elle ne se soit pas trompée. Il convient donc, avant de l'exécuter, de s'assurer qu'il est exempt d'erreurs et que son exécution sur la plate-forme hôte ne mettra pas en cause le bon fonctionnement de cette dernière, tant au niveau du temps de calcul qu'au niveau des ressources mémoires utilisées.

Il est démontré que si le code d'une fonction passe certaines vérifications avec succès, le temps et la taille mémoire nécessaires à son exécution peuvent être majorés polynômialement par rapport à la taille de ses arguments. Ces vérifications sont de quatre sortes ; outre l'analyse statique standard (vérification de type), une deuxième analyse, dite de forme, peut être faite sur le code. Les résultats obtenus à l'issue de ces deux premières vérifications sont ensuite combinés d'abord avec la vérification de taille - pour trouver une majoration de la taille mémoire nécessaire -, et enfin avec la vérification de terminaison pour montrer qu'on peut majorer le temps d'exécution du code.

Pour cela, le fournisseur du code l'accompagnera également d'annotations, notamment les certificats des fonctions qu'il aura définies dans son programme. Ces annotations seront confrontées au bytecode lors du chargement de ce dernier en mémoire, afin de l'authentifier.

Après avoir défini le langage qui sert de support à ces vérifications (section 2), la section 3 présentera un compilateur pour ce langage, en particulier, le fonctionnement du synthétiseur de types. La partie 4, quant à elle, définira une machine virtuelle permettant d'exécuter le bytecode généré. Enfin, on présentera dans la section 5 l'algorithme de Kildall, qu'on utilisera ensuite dans les sections 6.1 et 6.2 pour mener les deux premiers types de vérifications, objectifs de ce TER.

 2  Le langage étudié

On se place dans le cadre d'un petit langage fonctionnel typé du premier ordre. Dans ce langage, un programme est une suite de déclarations de types, suivie par des définitions de fonctions (éventuellement mutuellement récursives).

Définition 1   Un identificateur est une suite de caractères alphanumériques, commençant par une lettre alphabétique.

 2.1  Les types

Un type est défini par ses constructeurs, chacun étant accompagné des types auxquels il s'applique.

Définition 2   Soient T1, T2, ..., Tn et T des types. Un constructeur de type T et construit sur T1, T2, ..., Tn est une application de T1T2 ∗ … ∗ Tn dans T.

Remarque 1   Dans cette définition, n peut valoir 0. Dans ce cas, le constructeur ne prend aucun argument et est dit constant.
Remarque 2   Les types étant définis récursivement, on peut avoir T = Ti pour certaines valeurs de i.

Syntaxe de définition d'un type

La déclaration d'un type est introduite par le mot clé type suivi de son nom et de ses constructeurs séparés par le symbole pipe(|).

Exemple 1   (type et of sont des mots clés du langage)
  type tnat = Z | S of tnat;;
  type env = Nil | C of tnat * env;;
Dans le premier cas, on définit le type tnat des entiers naturels: un entier est soit zéro (Z), soit le successeur d'un entier (S(tnat)). Ainsi, S(S(S(S(Z)))) représente l'entier 4. Le deuxième cas définit le type env des listes d'entiers: Nil représente la liste vide, tandis que C(n,ls), où n est de type tnat et ls de type env, est la liste obtenue en insérant n en tête de ls. Ainsi, la liste [2; 0; 1; 2] sera représentée par C(S(S(Z)), C(Z, C(S(Z), C(S(S(Z)),Nil)))).

 2.2  Les fonctions

La définition d'une fonction est introduite par le mot clé function, suivi du nom de la fonction et de ses paramètres mis entre parenthèses. Ensuite vient le corps de la fonction, qui peut être soit une expression, soit un filtrage.

Remarque 3   Une fonction prend au moins un argument.

Définition 3   Une variable est un identificateur commençant par une lettre minuscule.

 2.2.1  Les expressions

Une expression est soit une variable, soit un constructeur appliqué éventuellement à des expressions, soit enfin une fonction appliquée à des expressions. Ainsi, les termes x, S(x), f(x1, x2, Z) sont des expressions valides.

 2.2.2  Les filtrages

Définition 4   Un motif est soit une variable, soit un constructeur appliqué éventuellement à d'autres motifs.

La syntaxe de définition d'un filtrage est la suivante (match et with sont des mots clés du langage):

match x  with
       m1exp1 |
       m2exp2 |
       … … … …
       mpexpp

Cette construction consiste à essayer d'identifier successivement la variable x avec les motifs m1, m2, ..., mp (dans cet ordre). En cas de réussite avec le motif mk (voir la définition 5 ci-dessous), alors, le filtrage est arrêté et l'expression expk est renvoyée. Si au contraire, aucune identification ne réussit, une erreur (une exception en fait) est retournée pour le filtrage.

Définition 5   On dit que le motif m filtre(ou identifie) x si et seulement si l'une des trois conditions suivantes est vérifiée:
  1. m est une variable
  2. x est affectée à C(p1, p2,…, pn), m est de la forme C(m1, m2,…,mk), C étant un constructeur, les pi et mi étant des motifs tels que pour tout i, le motif mi filtre pi.
Extension du filtrage : Au lieu de filtrer une variable x avec un motif m, on étend le filtrage à celui d'une liste de n variables par une liste de n motifs. Dans ce cas, l'identification réussit si et seulement si, pour tout i le i-ième motif filtre la i-ième variable (au sens de la définition 5).

Voici un exemple de fonction, add, qui calcule la somme de deux entiers positifs; elle utilise le type tnat défini dans la section 2.1 et un filtrage sur son premier argument.
Exemple 2  
function add(x,y) = match x with
Z -> y // si x = 0 add(x,y)=y
|
S(u) -> add(u, S(y)) //add(u+1,y)=add(u,y+1)
;;

 3  Un compilateur pour le langage

Le compilateur (fcompile.ml) génère, à partir d'un fichier source, d'une part le bytecode associé et d'autre part, un fichier de signatures récapitulant les prototypes de toutes les fonctions et des types définis dans le source.

Le bytecode se présente comme une suite d'instructions de la machine virtuelle (cf. section 4), qui possède 6 instructions: Load, Build, Branch, Return, Stop et Call.

Le synthétiseur de types a pour but de générer le prototype (type des arguments et type de retour) d'une fonction à partir de son code source, et par suite, de générer le fichier signatures.

Dans notre analyse, nous considérons deux sortes de types : Dans la suite, on dira que les types définis sont plus précis que les types polymorphes, ces derniers étant plus généraux.

Définition 6   Un type polymorphe est une variable de type qui peut être instanciée par des types différents, selon son utilisation.

Définition 7   Une fonction est polymorphe si au moins un des types de ses arguments ou son type de retour est polymorphe.

Définition 8   Une affectation de types est un couple de types (t, t'), dont le membre droit t' est au moins aussi précis que le membre gauche. Un tel couple sera noté quelque fois tt'.

Définition 9   On dit que deux types sont compatibles si et seulement si l'une des conditions suivantes est vérifiée :
Algorithme de compatibilité (entre les arguments effectifs d'une fonction g et le certificat de g)
Soit (args, rt)=([t1, t2, …, tm], t0) la signature de g, c'est-à-dire que g est de type t1t2→ …→ tmt0; soit en outre ls la liste des types des arguments effectifs de g et n sa taille. On considère une liste d'affectations de types subst, initialement vide.
La vérification de compatibilité entre (args, rt) et ls peut être décrite par l'algorithme suivant :
Compatibility(args, rt) ls = VerifyCompatibility(args, rt) ls [ ] avec
VerifyCompatibility (args, rtls subst :
si nm, alors erreur.
sinon
    si args = [ ] alors
         si ∃ a, (rt,a) ∈ subst alors a
         sinon rt
    sinon si args est de la forme (a::ls1) et ls de la forme (b::ls2), alors
          si (a = b) alors VerifyCompatibility (ls1, rtls2 subst
          sinon
             si a est une variable de type et si ∃ c, (a,c) ∈ subst alors
                 si b = c alors erreur
                 sinon VerifyCompatibility (ls1, rtls2 (a,b)::subst
Définition 10   L'unification de deux types retourne le plus précis des deux, s'ils sont compatibles. Dans le cas contraire, l'unification retourne une erreur.

Algorithme d'unification
:
Unification x y =
si x = y, retourne x
sinon si x est une variable de type, retourne y
sinon si y est une variable de type, retourne x
sinon erreur
Définition 11   A un instant donné, l'environnement de types est l'ensemble associant les variables connues à leurs types pendant l'analyse d'un bloc.

 3.1  Mise en oeuvre

Le synthétiseur de types (fsig.ml) prend en argument une fonction (son nom et son bytecode) et produit son prototype (si elle est bien typée), c'est-à-dire le type le plus général de ses arguments et son type de retour. Lors du calcul de ce dernier, des contraintes sont générées sur les types rencontrés. Ensuite, on en déduit les types les plus généraux, si le système obtenu est soluble. Ces contraintes sont de la forme :
contraintes ≡ (variable de type, type) ou (variable de type, variable de type)
A chaque ajout d'une nouvelle contrainte, la fonction add_equation (cf. sous-section 3.3) permet de réduire et de résoudre le système.

 3.2  Génération des contraintes et calcul du type de retour

Au départ, les arguments ont le type le plus général possible ; c'est pourquoi, dans l'implémentation, à chaque argument est associé une variable de type (initialisation de l'environnement).
Le corps d'une fonction est : Nous allons suivre cette organisation pour montrer, cas par cas, comment se déroule la génération de contraintes et comment le type de retour est calculé. Tout d'abord, il est à noter que le type de retour d'une fonction est le type de son corps. Résumons les contraintes obtenues lors de l'analyse :
Analyse de Match ( v, m, body1, body2) Analyse de l'expression c(e1,...,en), où c est un constructeur Analyse de l'expression f(e1,...,en), où f est un symbole fonctionnel On obtient donc un système d'équations correspondant aux contraintes générées. Il s'agit alors de le transformer en un système d'affectations de types. Ainsi, à la fin de la résolution de ce système, on aura obtenu, pour le type de chaque argument et pour le type de retour, le type le plus général possible tout en étant assez précis pour respecter l'ensemble des contraintes. Par ailleurs, les affectations obtenues à partir des contraintes sont :
Pour un filtrage Match(v, m, b1, b2) : Pour une expression c(e1, e2, …,en) :
Soient τ1, …, τn les types respectifs de e1,...,en, et soient t1, …, tn les types des arguments de c (récupérés dans la déclaration des types du programme source). On sait alors que t1, …, tn sont des types connus. Par conséquent, ils sont forcément plus précis que τ1, …, τn. Les égalités se traduisent par : ∀ i, 1 ≤ in, τiti.
Pour une expression f(e1,...,en) :
Soient t1, …, tn les types des arguments de la fonction f (récupérés dans la liste des signatures de fonctions). Soient τ1, …, τn les types respectifs de e1,...,en. Alors : ∀ i, 1 ≤ in,

 3.3  Résolution du système d'affectations

La résolution du système d'affectation se fait à chaque arrivée d'une nouvelle contrainte à l'aide de la fonction add_equation. A chaque ajout, on propage (par la fonction apply_equation) l'information apportée par celle-ci à toute la liste, afin d'obtenir une solution à ce système.
Préliminaires :
On définit la fonction transitive par:
transitive (t, t') (t1, t2ls = On définit également la fonction propa par:
propa ( t, t') [(t1, t1');…;(tn, tn')] =
res ← [ ]
Pour i de n à 1 faire
    transitive (t,t') (ti, ti') res
fin pour
retourner res
Algorithme apply_equation
Supposons que l'on souhaite rajouter le couple (x1, x2) à la liste d'affectation ls. Il y a plusieurs cas à distinguer :
apply_equation (t,t') ls = Et enfin, la fonction add_equation: elle rajoute une contrainte au système d'affectations déjà obtenu et propage la contrainte:
add_equation (t, t') ls =

 4  La machine virtuelle

Le but de la machine virtuelle (notée par la suite VM) est de calculer les valeurs de chacune des expressions données.
Elle dispose de six instructions : Branch, Build, Call, Load, Stop, Return. Cela peut paraître peu, voire insuffisant, surtout comparé au très grand nombre d'instructions que peuvent comporter certaines plate-formes telles que Java (184 instructions), mais cela est largement suffisant pour développer les principes de vérifications qui nous intéressent.
En effet, malgré cet aspect simpliste, elle regroupe les principales fonctions que l'on peut attendre : branchement conditionnel, appel de fonctions, etc... et cela est parfaitement adapté à la mise en exergue des conséquences d'une modification malicieuse de bytecode.
La machine virtuelle, que nous considérons ici, travaille uniquement sur une pile, appelée pile d'exécution. Avant l'évaluation du bytecode bf de chaque fonction f, on empile ses arguments. Puis, lors du traitement, la pile sera modifiée après analyse de chaque instruction de bf, et ce, en suivant le schéma ci-dessous :
  1. Branch (c, j) : branchement conditionnel (traduction du match)
    Identifie le sommet de la pile avec le constructeur c. Si le branchement réussit, c'est-à-dire que le sommet de la pile est de la forme c(a1,…,an), on dépile l'élément au sommet, puis on empile les arguments a1,…,an de c et l'instruction suivante est exécutée, sinon, on saute à l'instruction j.
  2. Build (c, n) : appel à un constructeur c d'arité n
    Les n arguments (arg1, …, argn) nécessaires à l'appel du contructeur sont dépilés (si la taille de la pile est insuffisante, la VM renverra une erreur). Ensuite, la valeur c(arg1,…,argn) est empilé.
  3. Call(f, n) : appel à une fonction f d'arité n
    On dépile les n éléments (a1, …, an ) en sommet de pile (si la pile n'est pas assez grande, la VM renvoie une erreur) puis on empile le résultat de l'évaluation de f(a1, …, an), celle-ci étant effectuée sur une nouvelle pile d'exécution.
  4. Load(i) : chargement d'une valeur d'adresse i
    La VM empile la i-ième valeur de la pile.
  5. Stop : arrêt de la machine
    La VM arrête l'analyse du bloc de la fonction en cours d'exécution. Un cas typique est l'apparition d'une erreur.
  6. Return : retour de résultat
    L'analyse du bloc en cours s'arrête et l'état courant est retourné.

 5  Vérification du bytecode : algorithme de Kildall

Le code d'une fonction sera certifié correct(pour un test donné) si chacune de ses instructions l'est. Pour cela, un état est associé à chaque instruction du code, et on dira que la p-ième instruction est correcte si elle concorde avec le p-ième état du programme, cette concordance étant dictée par la fonction de flot step définie ci-dessous. Les états vont dépendre du type de vérification effectuée et seront donc précisés (instanciés) dans les sections 6.1 et 6.2. Pour l'heure, notons qu'il y a un état particulier, l'état incohérent, noté ⊤ et dont la signification est assez explicite.

 5.1  Préliminaires

Dans la suite, on notera Σ l'ensemble des états et on appellera programme, le type des listes à n états, n étant le nombre d'instructions du code en cours de vérification.
On suppose que : Introduisons quelques notations: On définit alors:
Définition 12   Soit bcv: programme → programme, une fonction. bcv est un vérificateur de bytecode si et seulement si pour tout programme ss, si la vérification par bcv de ss est consistant, alors il existe un programme correct ts au plus aussi cohérent que ss :
ss ∈ programme, ⊤ ∉ (bcv ss)⇐⇒ (∃ ts ∈ programme, sstscorrecteprog(ts).

 5.2  L'algorithme de Kildall

L'algorithme qu'on présente ici et qu'on utilise pour les vérifications de type et de forme est dû à G. Kildall, et il est prouvé que si la fonction de flot step est monotone, alors il s'agit d'un analyseur de flot. La monotonie signifie ici que si s1 est plus cohérent que s2, alors step(s1) est plus cohérent que step(s2). Mais avant de détailler l'algorithme, définissons quelques notions:
Définition 13   (Stabilité d'une instruction)
stable: programme ∗ Nboolean.
Une instruction p est dite stable si et seulement si pour tout successeur q de p, le nouvel état associé à l'instruction q est au moins plus cohérent que l'ancien:
stable(ss,p)⇐⇒ ∀ ( q, s) ∈ step (p,ss[p]), sss[q]

Définition 14  (Stabilité d'un programme)
stableprog: programme → boolean.
Un programme est stable si et seulement si chacune de ses instructions est stable:
stableprog(ss) ⇐⇒ ∀ p, 1 ≤ pn stable(ss,p).

Définition 15   (analyseur de flots)
Soit dfa: programme → programme. dfa est un analyseur de flot de données si et seulement si pour tout programme ss, les 3 conditions suivantes sont vérifiées:

Il peut alors être démontré que si les fonctions stable et correct vérifient la condition (C) ci-dessous, alors un analyseur de flot est un vérificateur de bytecode. Ainsi, si stable et correct vérifient (C) et que step est monotone, alors l'algorithme de Kildall sera un vérificateur de bytecode.
(C) : ∀ ss∈ programme, ∀ p, 1 ≤ pn, (⊤∉ sscorrect(ss,p))⇐⇒ stable(ss,p).
Dans l'algorithme ci-dessous, wl est l'ensemble des instructions dont les états associés ont changé à la dernière itération. On suppose qu'on a une fonction replace qui prend en paramètre une liste ls à m éléments, un entier km et un élément elt et qui renvoie la liste obtenue à partir de ls, en remplaçant son k-ième élément par elt.
Algorithme Kildall
Fonctionnement de l'algorithme : wl est l'ensemble des numéros d'instructions non encore stables. L'idée est de partir d'un noeud instable et de calculer ses successeurs. S'il y en a parmi ceux-ci dont les états associés ont changé, c'est qu'ils ne sont pas encore stables et doivent donc être rajoutés à la work list. Le procédé est itéré jusqu'à ce que toutes les instructions soient stables. Ainsi, à la fin de l'algoritme(il est démontré qu'il termine), toutes les instructions de bcode sont stables.

 6  Applications de l'algorithme de Kildall : vérifications de type et de forme

On vient de voir un algorithme générique permettant de vérifier du bytecode. Comme mentionné au 5, la notion de correction du code dépend du type de vérification qu'on effectue. Les deux types de vérifications consistent en une exécution symbolique de la machine virtuelle définie en 4, les symboles manipulés étant des types pour la vérification statique et des expressions pour la forme. Après avoir défini formellement la notion d'état, on donnera pour chacune des deux, la fonction de flot step permettant de propager les états dans le programme.
Un état est soit l'état chaos(⊥), soit l'état incohérent(⊤), soit enfin une liste de symboles. On a donc :
Σ = { ⊥, ⊤ } ∪ S, où S est à préciser dans chaque type de vérification.
On définit alors la relation ≤ par: Puis la loi (commutative) sup par: Et enfin, la fonction succs donnant la liste des successeurs d'une instruction (n est la longueur du bytecode en cours d'analyse ):
Instruction p succs(p)
Return [p]
Stop [ ]
Branch c j si p + 1 < n et j < n alors [ p + 1 ; j ] sinon [p;p]
Load j si p + 1 < n alors [ p + 1 ] sinon [p]
Build c m si p + 1 < n alors [ p + 1 ] sinon [p]
Call g m si p + 1 < n alors [ p + 1 ] sinon [p]

Par ailleurs, pour deux listes l1 = [a1, …, an ] et l2 = [ b1, …, bn ] de même taille, on définit combine (l1, l2) comme étant la liste de couples [(a1, b1 ), …, ( an, bn )].

 6.1  Vérification de type

La vérification de type permet d'assurer la cohérence des types entre les signatures accompagnant le bytecode et les valeurs réellement manipulées. De plus, il permet de détecter les débordements mémoire dus à des opérations illégales (branchement à une adresse inexistante, chargement d'une variable à une adresse inconnue, ...)
Exemple 3   Considérons le cas de la fonction succ suivante (les types tnat et env sont définis en 2.1):
  (* succ : prend en argument un entier x et renvoie x+1 *)
  let succ (x) = S (x);;
Soit le byte code associé (obtenu après compilation de la fonction) :
  Load 0;
  Build (S,1);
  Return;
Sans la vérification de type, le résultat de l'évaluation de succ(Nil) serait S(Nil), c'est-à-dire Nil+1, ce qui est évidemment incorrect sémantiquement. Pour ce type de vérification, les symboles manipulés sont des listes de types.
Soient s l'état courant et p l'instruction à exécuter. La fonction de flot associée à la vérification de type est définie par :
step (p,s) = combine(succs p, fstep(p,s)), fstep étant définie ci-dessous selon l'instruction à exécuter.
  1. Branch c j :
    fstep (p,s) =
  2. Build c n :
    fstep (p,s) =
  3. Call g n :
    fstep (p,s) =
  4. Load j :
    fstep (p,s) =
  5. Stop : fstep (p,s) =[ ]
  6. Return :
    fstep (p,s) =
On peut introduire maintenant, la notion de correction d'une fonction : une instruction p est correcte par rapport à l'état courant σ si et seulement si ( σ ≠ ⊤ ) et ( ⊤ ∉ fstep( p, σ ) )
Soit f la fonction en cours d'analyse. Le programme (liste d'états) associé à f est correct si et seulement si toutes ses instructions sont correctes et que son premier état est de la forme [ t1, …, tn ] où n est l'arité de f, cette dernière prenant des paramètres de types t1, …, tn.

 6.2  Vérification de forme

Comme annoncé au début du paragraphe 6, la vérification de forme est axée sur la forme des expressions manipulées(et non pas sur les types). On suppose donc qu'une vérification de type a deja été faite sur le bytecode, et que toutes les fonctions définies sont bien typées.
Il s'agit donc de définir les états manipulés ainsi que la fonction de flot correspondant.

 6.2.1  Les états

Pour la vérification de forme, les symboles manipulés sont des expressions (au sens de la définition 2.2.1). Toutefois, on associe à chaque état une substitution, c'est à dire une liste de couples (v, e), où v est une variable et e une expression. Ces substitutions sont utiles pour mener les vérifications de taille et de terminaison. En notant S l'ensemble des substitutions et E celui des expressions, on a ici : Σ = {⊥,⊤ } ∪ ((E list) ∗ S)

 6.2.2  La fonction de flot

La fonction de flot permet de propager les états des instructions instables vers leurs successeurs dans le graphe de flot. On rappelle également que cette fonction est une exécution symbolique de la machine virtuelle. Il est donc naturel que la propagation dépende de l'instruction instable à considérer. On suppose que la machine se trouve dans un état s∈ Σ, qu'on traite l'instruction p et on va donner les règles de propagation de s, ainsi que la définition de la correction de l'instruction p: dans ce qui suit, σ, σi sont des substitutions, e, ei sont des expressions et ls une liste d'expressions. Pour deux listes l1 et l2, on notera l1@l2 la liste obtenue en concaténant les listes l1 et l2, l2 étant en tête, et σ1 ∘ σ2 représentera la composée(mathématique) des substitutions σ1 et σ2. On suppose l'existence d'une fonction new_vars qui prend en argument un entier k et qui renvoie k variables fraiches(non encore utilisées), et une fonction arity qui renvoie l'arité d'un constructeur.
fstep étant la fonction définie ci-dessous, la fonction de flot est définie par :
step(p,s)= combine(succs p, fstep (p,s)).
Comme dans le cas de la vérification de types, la fontion fstep est définie par cas, selon l'instruction à exécuter :
  1. Return :
    fstep (p,s) =
  2. Stop : fstep (p,s) = [ ]
  3. Load j :
    fstep (p,s) =
  4. Build c m :
    fstep (p,s) =
  5. Call g m :
    fstep (p,s) =
  6. Branch c label :
    fstep (p,s) =
On peut alors définir correct comme suit : une instruction est bien formée si et seulement si l'état associé est cohérent et qu'aucun de ses états successeurs n'est incohérent. Et un programme est bien formé si tous ses états sont consistants et si le premier état (celui associé à la première instruction) est de la forme ([v1, v2, …, vm], id), où m est l'arité de la fonction en cours d'analyse et id la substitution identité.

 7  Conclusion

Avec le développement des systèmes de télécommunications, il est très courant que du code soit échangé sous forme pré-compilé. Or, les personnes vectrices ou les moyens utilisés pour les échanges peuvent affecter (in)consciemment ce code. Il est donc de mise de le soumettre à des contrôles avant de le charger en mémoire sur le support hôte. Il devient donc essentiel d'établir des méthodes permettant de s'assurer de la validité du code transmis.
En s'appuyant sur l'algorithme de G. Kildall, on a développé deux types de vérifications sur le code : d'une part, la vérification de type, d'autre part celle de la forme des expressions manipulées.
Certes, ces deux analyses permettent de rejetter un grand nombre de programmes incorrects, et ainsi d'accroître considérablement la sûreté d'exécution; mais ils ne sauraient être considérées comme suffisantes, vu l'expansion du piratage informatique. Comme on l'a mentionné, d'autres tests pourraient encore être menés, en particulier les vérifications de terminaison et de taille, surtout si l'on sait qu'en général, les plate-formes sur lesquelles sont exécutés ces codes sont d'une capacité mémoire très réduite.

 8  Références

[AMA04] R. Amadio. Synthesis of max-plus quasi-interpretations. Research report. January 2004.

[ACZJ04] R. Amadio and S. Coupet-Grimal and S. Dal Zilio and Line Jakubiec. A functional scenario for bytecode verification of resource bounds. Research report. January 2004

[Kil73] G. A. Kildall. A unified approach to global program optimization. Boston, MA, Octobre 1973.

[Ler01] X. Leroy. On-card bytecode verification for Java Card. 2001.

[LY99] T. Lindholm and F. Yellin. The Java Virtual Machine Specification. Addison-Wesley, 1999.

[MWCG99] G. Morriset, D. Walker, K. Crary and N. Glew. From System F to Typed Assembly Language. ACM Transactions on Programming Languages and Systems,1999.

[Nec97] G. Necula. Proof carrying code, 1997.

[Nip01] T. Nipkow. Verified Bytecode verifiers. Foundations of Software Science and Computation Structures. Springer-Verlag, 2001.

[Ray01] D. Rayside. A generic worklist algorithm for graph reachability problems in program analysis. University of Waterloo, 2001.

[San01] D. Sannella. Mobile resource guarantee. Ist-global computing research proposal, U. Edinburgh, 2001. http://www.dcs.ed.ac.uk/home/mrg/.


This document was translated from LATEX by HEVEA.