Document de Synthèse

publicité
Document de Synthèse
Sujet de thèse:
Preuves formelles des programmes numériques en
prenant en compte l’architecture et le
compilateur
Thi Minh Tuyen NGUYEN
1
Objectifs et contributions
Sur des architectures récentes, un programme numérique peut donner des
réponses diférentes. La raison est qu’il existe des incohérences entre des exécutions du programme sur des matériels différents. Précisément, la norme IEEE754 – une norme pour la représentation des nombres en virgule flottante, supporté par la plupart des matériels – spécifie qu’un nombre flottant en double
précision est arrondi en 64 bits. Par contre, l’unité de calcul en virgule flottante
x87 (x87 FPU)– supportée par des processeurs de l’architecture IA32 – utilise
des registres en 80 bits. L’instruction FMA (fused multiply-add) – utilisée dans
les architectures PowerPC et Intel Itanium – calcule x × y ± z avec un seul
arrondi. En outre, l’optimisation peut causer des incohérences du résultat.
Le but de cette thèse est de prouver formellement un programme numérique
en prenant en compte l’architecture et le compilateur. Pour le faire, nous avons
proposé deux approches différentes:
• Une approche permet de prouver des propriétés des programmes en virgule
flottante qui sont vraies sur plusieurs architectures et compilateurs. Cette
approche ne considère que les erreurs d’arrondi qui doivent être validées
quels que soient l’environnement matériel et le choix du compilateur.
• La deuxième approche consiste à prouver des propriétés des programmes
en analysant leur code assembleur. Nous nous concentrons sur des problèmes et des pièges qui apparaissent sur des calculs en virgule flottante.
L’analyse directe du code assembleur nous permet de considérer des
caratéristiques dépendant de l’architecture ou du compilateur telle que
l’utilisation des registres en précision étendue.
1
2
Preuves formelles de programmes numériques
indépendant de l’environment
2.1
Borne pour une opération en virgule flottante
Le choix entre l’arrondi en 64 bits, en 80 bits et en double arrondi est la raison principale qui provoque l’incohérences du résultat. Nous prouvons une
borne d’erreur d’arrondi qui est valide quels que soient le matériel et l’arrondi
choisi. Nous désignons par ◦64 (x) l’arrondi au plus proche en 64 bits, par
◦80 (x) l’arrondi au plus proche en 80 bits, par ◦64 (◦80 (x)) le double arrondi.
Le théorème suivant est la base de notre approche pour prouver correctement
des programmes numériques quels que soient l’architecture et le compilateur:
Theorem 1 Soit un nombre réel x, soit (x) défini par ◦64 (x), ou ◦80 (x), ou
le double arrondi ◦64 (◦80 (x)). Alors,
−1022
Si |x| ≥ 2
x − (x) −64
−1022
et |(x)| ≥ 2
alors ≤ 2050 × 2
x
!
Si |x| ≤ 2−1022 alors |x − (x)| ≤ 2049 × 2−1086 et |(x)| ≤ 2−1022
2.2
2.2.1
Preuve de programme numérique
FMA
Considérons que nous avons (x) = x. Un FMA est alors ((a × x) + b) avec
le intérieur qui est l’arrondi identité. Le théorème 1 est donc valide avec le
FMA.
2.2.2
Borne d’une séquence des opérations
Theorem 2 Soit une opération parmi addition, soustraction, multiplication,
division, racine carrée, négation et valeur absolue. Soit x = (y, z) le résultat
exact de cette opération (sans arrondi). Alors, quelle que soit l’architecture et
la compilation, le résultat calculé x̃ est
Si |x| ≥2−1022 , alors
x̃ ∈ x − 2050 × 2−64 × |x| , x + 2050 × 2−64 × |x| \ −2−1022 , 2−1022 .
Si |x| ≤2−1022 , alors
x̃ ∈ x − 2049 × 2−1086 , x + 2049 × 2−1086 ∩ −2−1022 , 2−1022 .
Theorem 3 Si nous considérons les formules du théorème 1 sur chaque opération (addition, soustraction, multiplication, division, racine carrée, négation,
valeur absolue), l’erreur d’arrondi finale est correcte quelle que soit l’architecture
et la compilation, à condition que le compilateur respecte l’ordre des opérations.
2
2.3
Quand le compilateur réorganise des additions
Theorem 4 Soit n un entier tel que n ≤ 1ε , soit (ai )0≤i≤n une séquence des
nombres réels et I un réel. Supposons que nous mettons dans la post-condition:
x ⊕ y est un nombre réel r tel que
|r − (x + y)| ≤ εn · (|x| + |y|) + n · η.
Pn
et pour un ordre o1 des additions, nous pouvons déduire que |Sno1 − 0 ai | ≤ I.
Maintenant si nous mettons dans la post-condition: x ⊕ y est un nombre réel
r tel que
|r − (x + y)| ≤ ε · |x + y| + η.
Pn
Alors, quel que soit l’ordre o2 des additions, nous avons |Sno2 − 0 ai | ≤ I.
2.4
Implémentation
Cette approche est implantée dans la plate-forme Frama-C pour l’analyse statique de code C.
Un point intéressant du plug-in Jessie est que nous pouvons changer
l’interprétation des opérations en virgule flottante facilement. Nous définissons
deux “pragmas” dite multirounding(multiroundingR dans le cas le compilateur
réorganise des additions) et changeons les post-conditions des opérations flottantes (addition, soustraction, multiplication, division, racine carrée, négation
et valeur absolue) et nous mettons les formules du théorème 1 (théorème 4) à
la place pour chercher l’erreur d’arrondi de l’ensemble du programme.
3
3.1
Preuve formelle de programme numérique
dépendant de l’architecture et le compilateur
Vue générale
Notre approche permet de prover un programme C en analysant son code assembleur. Pour le faire, il faut suivre les étapes illustrées dans la Figure 1. Toutes
les étapes nécessaires pour prouver un programme avec son code assembleur
sont présentées dans la figure à gauche. La figure à droite instancie ces étapes
pour la preuve d’un programme foo.c.
Dans un programme C annoté par ACSL (ANSI/ISO C Specification Language), toutes les annotations sont mises dans les commentaires. Quand gcc
génère le code assembleur, ces annotations seront ignorées. Car les annotations
sont importantes pour prouver un programme, une étape de préparation est
donc nécessaire. Cette étape met toutes les annotations sous forme assembleur
inline pour que ces annotations apparaîssent dans le code assembleur généré.
Cette étape est implantée par la réalisation d’un traducteur.
Une fois que l’étape de préparation est fait, un autre fichier C contenant
assembleur inline est généré. En suite, nous utilisons gcc -S pour générer le
code assembleur à partir de ce fichier.
3
La traduction du code assembleur vers Why est implantée dans notre version
modifiée de GAS (GNU Assembler). Cette étape génère un fichier contenant les
obligations de preuve dans Why. Puis, ces obligations de preuve sont tentées de
prouver par des prouveurs automatiques/interactifs.
Progamme en C
+ annotations en ACSL
foo.c
préparation de code
./inlineasm foo.c
Programme en C
+ assembleur inline
foo_inline.c
gcc -S foo_inline.c
compilation de C
foo_inline.s
Code assembleur
assembleur modifié
./as-new foo_inline.s
foo_inline.why
Obligations de preuve en Why
preuve
gwhy foo_inline.why
Prouvers automatiques
/prouvers interactifs
Prouvers automatiques
/prouvers interactifs
Figure 1: Étapes pour traduire un programme C vers Why
3.2
3.2.1
Traduction vers Why
Modèle de données
Entier machine et nombres en virgule flottante La logique Why n’a
que des entiers et des réels non bornés. Nous réutilisons le modèle des entiers
machines fournis par le plug-in Jessie de Frama-C. Voici le modèle des entiers
en 32 bits. Celui en 64 bits est fait de la même façon.
type int32
logic integer_of_int32: int32 -> int
predicate is_int32(x:int) = -2147483648 <= x and x <= 2147483647
axiom int32_coerce: forall x:int32. is_int32(integer_of_int32(x))
Le type abstrait int32 pour les entiers en 32 bits est déclaré, avec une
fonction integer_of_int32 retournant la valeur qu’un entier machine désigne.
Le prédicat is_int32 vérifie si un entier se situe dans l’intervalle d’un entier en
4
32 bits, et nous posons un axiome pour spécifier que la valeur désignée par un
int32 est toujours dans cet intervalle.
Afin de représenter des nombres flottants, nous réutilisons le modèle des
nombres flottants en 32 et en 64 bits qui est défini par Ayad and Marché [1].
Ce modèle introduit les types abstraits single and double. Nous pouvons
également représenter le type binary80 pour un nombre flottant en 80 bits.
Voici le modèle pour les nombres flottants en 64 bits:
type mode = nearest_even | to_zero | up | down | nearest_away
type double
logic double_value : double -> real
logic round_double : mode, real -> real
predicate no_overflow_double(m:mode,x:real) =
abs(round_double(m,x)) <= 0x1.FFFFFFFFFFFFFp1023
Un type énuméré mode est défini pour cinq modes possibles d’arrondi. Le
type abstrait double pour les nombres flottants en 64 bits est déclaré, avec une
fonction double_value retournant la valeur réelle qu’il désigne. La fonction
logique round_double(m, x) retourne le nombre représentable en 64 bits après
avoir arrondi avec le mode m. Le prédicat no_overflow_double(m, x) vérifie
si un réel x arrondi en 64 bits avec le mode m est débordé.
Registres Une caractéristique importante de notre approche est comment
modéliser les registres que les instructions assembleurs utilisent. Le problème
est qu’un registre n’enregistre qu’une séquence des bits, qui peut être interpreter
comme un entier, un nombre flottant ou une adresse mémoire. En plus, nous
considérons qu’il n’y a pas de différence entre un registre en 64 bits et sa valeur
en 32 bits enregistrée dans la partie inférieure. Par exemple: il n’y a pas de
différence entre les registres rax et eax dans l’architecture x86.
Pour modéliser cet comportement, nous introduisons un type abstrait
register équipé avec quelques symboles d’accessibilité. Chaque symbole
désigne une “vue” différente de la valeur enregistrée dans le registre. Par exemple, afin de modéliser \exact, le symbole sel_exact est la vue pour le calcul
avec la précision illimitée.
type register
logic sel_int32
logic sel_int64
logic sel_single
logic sel_double
logic sel_80
logic sel_exact
:
:
:
:
:
:
register
register
register
register
register
register
->
->
->
->
->
->
int32
int64
single
double
binary80
real
Pour chaque registre, nous introduisons une variable Why avec le type
register.
5
3.2.2
Traduction des opérandes
Nous voulons traduire des opérandes étant les registres ou les références mémoires vers des variables Why. Pour le faire, nous suivons l’hypothèse suivante:
Assumption 5 (Hypothèse de Séparation) Dans un programme C simple,
le compilateur génère un code assembleur dans lequel les références mémoires
distinctes syntaxiquement désignent les mémoires disjointes.
Cette hypothèse de séparation nous permet de traduire chaque référence
mémoire vers une variable Why dont le nom est dérivé de la référence.
Par exemple, supposons que dans le code assembleur, deux références mémoires -16(%rbp) and -8(%rax) soient disjointes. Cette hypothèse n’est plus
vraie dans le modèle mémoire dans la section 3.4.
Un opérande est soit une constante, un registre ou une référence mémoire.
Les instructions simples pour copier (commencer par mov) et les opérations
arithmétiques ont un opérande de sortie destination et un ou plusieurs opérandes
d’entrée sources.
Il y a 5 interprétations différentes d’un opérande de source dépendant du type
de la valeur attendue. Nous désignons par JoprKint32 , JoprKint64 , JoprKsingle ,
JoprKdouble et JoprKbinary80 l’interprétation d’une opérande de source, respectivement des entiers en 32 bits, en 64 bits et des nombres flottants en 32, en 64
et en 80 bits. Nous désignons également par JoprKexact la valeur abstraite de
\exact.
JimmKint32
JimmKint64
JimmKsingle
JimmKdouble
JregmemKint32
JregmemKint64
JregmemKsingle
JregmemKdouble
JregmemKbinary80
JregmemKexact
=
=
=
=
=
=
=
=
=
=
imm
imm
decode_float32 (imm)
decode_float64 (imm)
integer_of_int32(sel_int32(!regmem))
integer_of_int64(sel_int64(!regmem))
single_value(sel_single(!regmem))
double_value(sel_double(!regmem))
binary80_value(sel_80(!regmem))
sel_exact(!regmem)
Les notations decode_float32 et decode_float64 ne sont pas de fonctions
logiques Why mais elles dénotent les opérations pour transformer un décimal
litéral vers un réel qu’il représente dans le format single et double. Ce décodage est fait à la compilation dans notre traducteur du code assembleur vers
Why.
3.2.3
Traduction des fonctions annotées
Supposons que nous avons une fonction avec les préconditions, les postconditions et les assertions. La traduction de cette fonction du langage assembleur vers Why est illustrée dans la Figure 2. Notre étape de préparation
6
f:
.cfi_startproc
/*@ requires P ; */
(body of the function f)
/*@ ensures Q; */
leave
ret
.cfi_endproc
−→
let f() =
−→
−→
−→
assumes {JP Kterm };
J (body of the function f) Ki
assert {JQ Kterm };
void
parameter f: unit ->
{ JP Kterm } unit writes w { JQ Kterm }
Figure 2: Traduction d’une fonction en assembleur vers Why
a déplacé la post-condition à la fin de la fonction. De manière plus générale,
chaque annotation est pré-traitée sous forme assembleur inline:
asm("/* <keyword> P */"::"X"(x0 ),..,"X"(xn ));
dans laquelle chaque variable xi dont le type τ dans la proposition P est remplacée par #τ #%i#. Le compilateur transforme cet assembleur inline vers des
lignes entre #APP et #NO_APP dont chaque %i est remplacé par une référence
de mémoire ou un registre, ou un registre de la pile en 80 bits.
Nous désignons par JAKterm la traduction d’un terme (expression logique)
vers Why. La traduction des annotations vers Why est la suivante
JA ==> B Kterm
JA == B Kterm
JA && B Kterm
JA || B Kterm
J!AKterm
J#int#v#Kterm
J#long#v#Kterm
Je1 op e2 Kterm
Je1 / e2 Kterm
Je1 % e2 Kterm
Je1 op e2 Kterm
Je1 != e2 Kterm
J\forall τ i; P Kterm
J\exists τ i; P Kterm
3.2.4
=
=
=
=
=
=
=
=
=
=
=
=
=
=
JAKterm -> JB Kterm
JAKterm = JB Kterm
JAKterm and JB Kterm
JAKterm or JB Kterm
not( JA Kterm )
JvKint32
JvKint64
Je1 Kterm op Je2 Kterm where op ∈ {+,-,*}
computer_div(Je1 Kterm , Je2 Kterm )
computer_mod(Je1 Kterm , Je2 Kterm )
Je1 Kterm op Je2 Kterm where op ∈ {>,<,>=,<=}
Je1 Kterm <> Je2 Kterm
forall i :Jτ Ktype . JP Kterm
exists i :Jτ Ktype . JP Kterm
Traduction des instructions
Instructions universelles Des instructions de déplacement et celles
d’addition/soustraction/multiplication/division en 32 bits sont traduites grâce
aux fonctions logiques suivantes:
parameter set_int32_no_check: imm:int -> dest: register ref ->
7
{ }
unit writes dest
{ integer_of_int32(sel_int32(dest)) = imm }
La fonction abstraite précédente met un entier en 32 bits à un registre sans
vérifier si cette valeur est débordée.
parameter set_int32: imm:int -> dest: register ref ->
{ is_int32(imm) }
unit writes dest
{ integer_of_int32(sel_int32(dest)) = imm }
La post-condition de set_int32 est identique à set_int32_no_check. Sa précondition vérifie si imm est un entier en 32 bits.
Nous désignons par J ins Ki la traduction Why d’une instruction ins. Les
instructions universelles dans le code assembleur sont interpretées vers Why
comme suit:
J
J
J
J
J
movl src, dest Ki
addl src, dest Ki
subl src, dest Ki
imull src, dest Ki
call label Ki
=
=
=
=
=
set_int32_no_check JsrcKint32 dest
set_int32 (JdestKint32 + JsrcKint32 ) dest
set_int32 (JdestKint32 - JsrcKint32 ) dest
set_int32 (JdestKint32 * JsrcKint32 ) dest
label _parameter()
La traduction des instructions en 64 bits est faite de même façon que celle
en 32 bits.
Chaque assertion est considérée comme une instruction. La traduction d’une
assertion A est la suivante
J /*@ assert A;*/ Ki = assert JAKterm ;
Instructions arithmétiques SSE2 Les instructions pour les opérations
arithmétiques de la famille SSE opèrent sur les entier ou les nombres flottants en
32 ou en 64 bits, dépendant du suffixe. La destination est toujours un registre.
Voici l’interprétation de la multiplication, les autres opérations sont similaires
(avec une division, la précondition vérifie que la diviseur n’est pas zero).
J mulss src, reg Ki
=
J mulsd src, reg Ki
=
set_single (JdestKsingle *JsrcKsingle )
(JdestKexact *JsrcKexact ) reg
set_double (JdestKdouble *JsrcKdouble )
(JdestKexact *JsrcKexact ) reg
parameter set_single : a:real -> exact:real -> b:register ref ->
{ no_overflow_single(nearest_even,a) } unit writes b
{ single_value(sel_single(b)) = round_single(nearest_even,a) and
sel_exact(b) = exact }
parameter set_double : a:real -> exact:real -> b:register ref ->
{ no_overflow_double(nearest_even,a) } unit writes b
{ double_value(sel_double(b)) = round_double(nearest_even,a) and
sel_exact(b) = exact }
8
Les préconditions de ces procédures vérifient un débordement. En plus, la
post-condition de set_double applique l’arrondi en virgule flottante de même
façon que nous interprétons la norme IEEE-754: “le résultat de la multiplication
doit être la même qu’il est d’abord calulé avec la précision illimitée et puis
arrondi vers le format du résultat”.
Instruction arithmétique x87 L’unité de calul en virgule flottante x87 a 8
registres flottants pour effectuer des nombres flottants en 80 bits. Ces registres
sont organisés comme une pile ST0–ST7 et le sommet de la pile est identifié par
un registre spécial. Dans le code assembleur, %st ou %st(0) désigne le sommet
de la pile tandis que %st(i) désigne le i-ième registre au dessous du sommet.
Nous représentons la pile par 8 variables st0, . . . , st7 dont le type est
register ref. La seule hypothèse est que la pile est vide à l’entrée et la sortie.
Notre traducteur calcule la valeur du sommet de la pile à chaque instruction.
Cette valeur doit être unique quel que soit le chemin du graphe de contrôle pour
atteindre cette instruction. Nous traduisons donc les registres x87 %st(i) vers
les variables Why sti dans lesquelles i = top_of _stack − i.
Les instructions pour charger la pile et enregistrer de la pile sont interpretées
comme suit:
J
J
J
J
fldl src Ki
fld st(%i) Ki
fldl1 Ki
fstl src Ki
=
=
=
=
set_80 JsrcKdouble JsrcKexact st0
set_80 JstiKbinary80 JstiKexact st0
set_80 (1.0) (1.0) st0
set_double Jst0Kbinary80 Jst0Kexact !src
La fonction logique set_80 pour les nombres flottants en 80 bits est définie par
parameter set_80: a:real -> aexact:real -> b:register ref ->
{ no_overflow_binary80(\nearest_even,a) }
unit writes b
{ binary80_value(sel_binary80(b)) = round_binary80(nearest_even,a)
and
sel_exact(b) = aexact }
Les instructions arithmétiques dans la pile sont interpretées comme suit (les
instructions fmul, fadd et fsub sont interpretées de même façon).
J fmull src Ki
=
J fmul %st(i), %st(j) Ki
=
set_80 (Jst0Kbinary80 *JsrcKdouble )
(Jst0Kexact *JsrcKexact ) st0
set_80 (JstjKbinary80 *JstiKbinary80 )
(JstjKexact *JstiKexact ) stj
Instructions FMA La traduction des instructions FMA est spécifiée comme
suit:
9
J vfmaddss src3,src2,src1,dest Ki
= set_single (Jsrc1Ksingle ∗Jsrc2Ksingle +Jsrc3Ksingle )
(Jsrc1Kexact ∗Jsrc2Kexact +Jsrc3Kexact ) dest
J vfmaddsd src3,src2,src1,dest Ki
= set_double (Jsrc1Kdouble ∗Jsrc2Kdouble +Jsrc3Kdouble )
(Jsrc1Kexact ∗Jsrc2Kexact +Jsrc3Kexact ) dest
J vfmsubss src3,src2,src1,dest Ki
= set_single (Jsrc1Ksingle ∗Jsrc2Ksingle -Jsrc3Ksingle )
(Jsrc1Kexact ∗Jsrc2Kexact -Jsrc3Kexact ) dest
J vfmsubsd src3,src2,src1,dest Ki
= set_double (Jsrc1Kdouble ∗Jsrc2Kdouble -Jsrc3Kdouble )
(Jsrc1Kexact ∗Jsrc2Kexact -Jsrc3Kexact ) dest
J vfnmaddss src3,src2,src1,dest Ki = set_single (-(Jsrc1Ksingle ∗Jsrc2Ksingle )+Jsrc3Ksingle )
(-(Jsrc1Kexact ∗Jsrc2Kexact )+Jsrc3Kexact ) dest
J vfnmaddsd src3,src2,src1,dest Ki = set_double (-(Jsrc1Kdouble ∗Jsrc2Kdouble )+Jsrc3Kdouble )
(-(Jsrc1Kexact ∗Jsrc2Kexact )+Jsrc3Kexact ) dest
J vfnmsubss src3,src2,src1,dest Ki = set_single (-(Jsrc1Ksingle ∗Jsrc2Ksingle )-Jsrc3Ksingle )
(-(Jsrc1Kexact ∗Jsrc2Kexact )-Jsrc3Kexact ) dest
J vfnmsubsd src3,src2,src1,dest Ki = set_double (-(Jsrc1Kdouble ∗Jsrc2Kdouble )-Jsrc3Kdouble )
(-(Jsrc1Kexact ∗Jsrc2Kexact )-Jsrc3Kexact ) dest
3.3
Traduction des instructions complexes
Dans le code source C, si nous avons des instructions complexes telles que des
instructions conditionnelles et des boucles alors le code assembleur contient des
sauts conditionels vers des labels. Nous ne pouvons pas interpreter de telles
sauts directement vers Why car son langage de programmation ne supporte que
des instructions structurées. Nous interpretons le graphe de contrôle du code
assembleur vers un ensemble fini des morceaux de code dans lequel les loop
invariants jouent un rôle de précondition et post-condition. L’algorithme de
cette interprétation est le suivant:
algo generateWhy(g:CFG) : List of Why functions
var done : array[node] of boolean
(* traverse_from(n) will be called on each node n of the CFG of type pre
or inv. using array done, we ensure that each of such node is treated
only once *)
recursive traverse_from(n:node) : Why expression;
(* explore(n,pre) will generate all the necessary Why functions to
encode the subgraph of the CFG starting from node n, with
precondition pre *)
procedure explore(n:node, pre:Why predicate)
var visited : array[node] of boolean
(* explore_rec(n,prefix) traverses the CFG from node n and produces
the Why function to encode the subgraph starting from n, assuming
10
that prefix is the list of statements which encodes the path which
arrives to n *)
recursive explore_rec(n:node,prefix:Why expression)
if visited[n]: fail (hypothesis not satisfied)
visited[n] <- true;
switch n.content_tag :
case instruction(i) :
explore(n.succ, prefix :: why_instr(i))
case assert(p) :
explore(n.succ, prefix :: assert (why_pred(p)))
case pre(p) :
explore(n.succ, prefix :: assume (why_pred(p)))
case post(p) :
explore(n.succ, prefix :: assert (why_pred(p))
case inv(p) :
produce_why_fun (prefix:: assert why_pred(p));
traverse_from(n)
case jump(l):
explore_rec (l, prefix)
case jump(c,l) :
explore_rec (l, prefix :: assume (why_cond(c))) ;
explore_rec (n.succ, prefix :: assume (not why_cond(c)))
case conditional_move(i,c,l):
explore_rec (l, prefix :: assume (why_cond(c)) :: why_instr(i)) ;
explore_rec (n.succ, prefix :: assume (not why_cond(c)))
If n is the last node of the subgraph then
produce_why_fun (prefix)
end explore_rec
visited[i] <- false for each node i;
explore_rec(n,[assume pre]);
end explore
if done[i] return;
done[i] <- true;
switch n.content_tag
case pre(p): explore(n.succ, why_pred(p));
case inv(p): explore(n.succ, why_pred(p));
default : impossible
end traverse_node
main:
done[i] <- false for each node i;
traverse_from(0).
11
3.4
Modèle de mémoire
Avec le modèle précédent, nous ne pouvons pas prouver des programmes qui
contiennent des tableaux, des pointeurs. Dans cet partie, nous proposons un
autre modèle qui permet de prouver de tel programme.
3.4.1
Définition de modèle de mémoire
Ce modèle utilise la théorie des tableaux. Afin d’accéder une valeur à une
adresse dans la mémoire, nous avons besoin d’une variable dont le type est
déclaré comme suit:
type ’v memory
logic select: ’v memory -> int -> ’v
La fonction logique select est une fonction pour accéder un élément à une
adresse spécifiée par un entier.
La mémoire est modélisée comme un tableau des registres. Nous la définissons par une variable suivante:
parameter MEM: register memory ref
Dans l’assembleur, une référence mémoire est un opérande dont la forme
générale est disp(base, index, scale) dans laquelle base et index sont des registres, disp et scale sont des constantes. La valeur par défaut de index est 0
et celle de scale est 1. Une référence mémoire mem = d(b, i, s) est interpretée
comme un entier b + d + i × s.
3.4.2
Interprétation des instructions assembleurs
Opérandes Un opérande étant soit une constante ou un registre est traduit
vers Why comme dans la partie précédente. Nous ne traduissons que des
opérands étant des références mémoires.
Jd(b, i, s)Kaddr
Jd(%rbp, i, s)Kaddr
Jsymbol(%rip)Kaddr
JmemKint32
JmemKint64
JmemKsingle
JmemKdouble
JmemKexact
=
=
=
=
=
=
=
=
JbKint64 +d+s*JiKint64
_rbp + d + s*JiKint64
symbol
integer_of_int32(sel_int32(select(MEM,JmemKaddr )))
integer_of_int64(sel_int64(select(MEM,JmemKaddr )))
single_value(sel_single(select(MEM,JmemKaddr )))
double_value(sel_double(select(MEM,JmemKaddr )))
sel_exact(select(MEM,JmemKaddr ))
Instructions
de
déplacement Nous définissons un paramètre
move_mem_to_reg64 pour déplacer les données en 64 bits d’un registre
à la mémoire:
12
parameter move_reg_to_mem64: a:register -> b:int->
{ }
unit writes MEM
{ integer_of_int64(sel_int64(select(MEM, b)))=integer_of_int64(sel_int64(a))
and
double_value(sel_double(select(MEM, b)))=double_value(sel_double(a))
and
sel_exact(select(MEM, b))=sel_exact(a)
and
unchanged_mem(MEM, MEM@, b, 8)
}
Le signe @ dans la post-condition Why désigne la valeur d’une variable avant
l’appel de la procédure.
Le prédicat unchanged_mem est défini par
predicate unchanged_mem(MEM1: register memory,MEM2:register memory,
addr:int,nb:int) =
(forall i:int. i<= addr-8 or i>=addr+nb -> select(MEM1,i) = select(MEM2,i)
)
and
(forall i:int. i<=addr-4 ->
integer_of_int32(sel_int32(select(MEM1, i))) =
integer_of_int32(sel_int32(select(MEM2, i)))
and
single_value(sel_single(select(MEM1, i))) =
single_value(sel_single(select(MEM2, i)))
)
La traduction des instructions mov est spécifiée comme suit:
J movq mem, reg Ki
J movq reg, mem Ki
4
=
=
move_cte64 JmemKint64 JmemKdouble JmemKexact reg
move_reg_to_mem64 !reg JmemKaddr
Conclusions et perspectives
Cette thèse a proposé deux approches pour prouver formellement un programme
prenant en compte l’achitecture et la compilation. La première approche est de
prouver des programmes numériques quel que soit l’environment. La deuxième
approche prouve des programmes en analysant leur code assembleur. Elle est
réalisée par environ 10k lignes du code C dans une version modifiée de GAS.
Les perspectives envisagées sont
• La première approche: Prendre en compte
– l’ordre des multiplications
– la distributivité
13
– le remplacement la division par la multiplication par l’inverse
• La deuxième approche:
– Considérer les valeurs spéciales: NaN, infinités, etc., traiter d’autres
modes d’arrondi
– Essayer d’utiliser un seul modèle: modèle mémoire
– Étudier le changement du code assembleur quand le programme est
compilé avec gcc -funsafe-math-optimizations
– Étudier si nous pouvons utiliser des assertions dans le programme C
au lieu d’utiliser assembleur inline
– Tester sur d’autres architectures
– Améliorer l’algorithme WP pour les programmes non-structurés
– Intégrer cette approche dans un compilateur certifié.
References
[1] A. Ayad and C. Marché. Multi-prover verification of floating-point programs.
In J. Giesl and R. Hähnle, editors, Fifth International Joint Conference on
Automated Reasoning, volume 6173 of Lecture Notes in Artificial Intelligence, pages 127–141, Edinburgh, Scotland, July 2010. Springer.
14
Téléchargement