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