Conception Formelle |
Dans le cadre de ce TP (et des suivants), nous utiliserons la version 27.1 de Frama-c, avec la version 2.5.2 d’alt-ergo. C’est celle qui est installée au CREMI et celle sur laquelle les présents TP ont été testés. Par ailleurs le solveur Z3 en version 4.12.2 est également installée, ce qui vous permettra de l’utiliser également parallèlement à alt-ergo. Toutes ces versions sont celles de l’été 2023 (date de mise à jour d’opam au CREMI).
Pour travailler au CREMI, connectez-y vous grâce à x2go, comme expliqué ici. Ensuite, comme frama-c y a été installé via opam, il n’est pas disponible de suite. Pour l’utiliser, vous devez suivre les points suivants: dans un terminal, tapez ’export OPAMROOT=/opt/local/opam’ puis ’eval $(opam env)’. Vous aurez à faire ces deux étapes à chaque fois que vous lancerez un nouveau terminal. Une fois cela fait, frama-c sera accessible via la commande ’frama-c-gui’. Vous pouvez bien évidemment inclure ces deux commandes dans votre .bashrc pour ne pas avoir à les taper quand vous lancez un terminal, ou faire un script. Ces informations sont rappelées sur la page du cours.
Vous aurez une autre manipulation à faire (mais une seule fois) pour que frama-c reconnaisse les prouveurs : ’why3 config detect’ (après avoir tapé les commandes précédentes).
Récupérer l’archive suivante et décompressez-la.
Si vous souhaitez travailler chez vous, vous devrez installer frama-c. Il est vivement conseillé de le faire via opam plutôt que par le gestionnaire de paquets de votre système. La marche à suivre est détaillée pour chaque système d’exploitation ici. N’hésitez pas à consulter les instructions détaillées en cas de problème. Par contre attention, cela installera les versions les plus récentes, donc il est possible que si vous faites cela à distance de la rédaction de ce sujet, vous n’ayez pas les même versions qu’au CREMI. Pour avoir les mêmes, vous pouvez, dans la dernière ligne (celle qui installe frama-c) remplacer ’frama-c’ par ’z3.4.12.2 alt-ergo.2.5.2 frama-c.27.1’. Cela installera les mêmes versions qu’au CREMI et devrait fonctionner sans problème.
Dans tous les cas, même si vous travaillez chez vous, les pas listés pour travailler au CREMI (excepté la commande export qui n’est utile qu’au CREMI) restent nécessaires.
À noter qu’il existe un outil de frama-c dont nous n’avons pas discuté ici, Eva, qui réalise une analyse appelée interprétation abstraite sur le code. Cette fonctionnalité n’est utile que sur des programmes complets (du moins, dans son implémentation actuelle) et ne sera pas utilisée dans le cadre de ce cours.
Cet outil est plus utile pour explorer un programme inconnu, et donne plutôt des garanties sur des exécutions particulières.
Il sera néanmoins lancé si vous regardez les occurrences d’une variable (pensez à réinitialiser le fichier avant de lancer WP si vous le faites).
Un contrat de fonction se note en commentaire dans l’en-tête de la fonction. Pour être reconnu par Frama-C, le commentaire doit commencer par un @ (juste après le /* ou le //).
Un contrat peut contenir une ou plusieurs clauses ensures, indiquant une propriété devant être vraie à la sortie de la fonction. Cette clause (entre autres) peut mettre en relation les arguments de la fonction (sans précision, le contrat parlera de leur valeur lors de l’appel de la fonction), et la valeur de retour de la fonction avec le mot clé \result. Il est également possible d’y utiliser des variables et des constantes globales. Leur valeur sera évaluée dans le contexte de retour de la fonction.
Exemple: /*@ ensures \result == a + b;*/
Nous verrons rapidement plus de constructions.
Un contrat de fonction peut être placé dans un fichier header (.h). C’est ce que nous ferons dans la plupart des cas (notamment dans cet exercice), notamment pour ne pas modifier des fichiers de code que l’on souhaite spécifier. Il est même possible de placer les spécifications dans un fichier non directement inclus dans le code à vérifier (en l’indiquant à Frama-C) pour importer la spécification dans un code préexistant sans modifier ses fichiers .h directement.
Il y a plusieurs manières d’écrire une spécification pour une fonction. Néanmoins, certaines sont à mon avis moins commodes et moins informatives – et également moins extensibles à des cas généraux. Une règle générale est selon moi que les implications devraient être réservées à des comportements très différents des fonctions et non à des cas qui en réalité décrivent une même fonction (typiquement, un maximum peut s’exprimer par des propriétés sur le résultat). La raison profonde, c’est que les implications (ou, en expression informelle, les phrases du style si ... alors ...) c’est plus dur à comprendre pour un humain qu’une propriété globale. Évidemment, il y a des cas où on ne coupera pas aux implications.
Une précondition est une propriété que l’on suppose vraie à l’entrée de la fonction. Les preuves seront faites en supposant que la propriété est vraie. Bien sûr, pour que le programme soit correct, toute fonction appelante doit respecter cette précondition (et donc, il faut la prouver là).
La précondition se note en ACSL avec le mot-clé requires. Les clauses requires doivent être placées avant les clauses ensures dans un contrat de fonction. Elles ne peuvent pas parler de \result (puisqu’il n’existe pas avant l’appel) et les variables qui y sont utilisées seront évaluées dans le contexte d’appel de la fonction.
Exemple: /*@ requires a + b >= 3;*/
Les bad_call (à part le premier) de l’exemple précédent sont des exemples d’appel où le comportement de l’addition et de la division ne sont pas spécifiés par la norme du C. Cela signifie que les compilateurs compilant du C peuvent adopter n’importe quel comportement pour ces appels en respectant la sémantique du C. De tels appels ne devraient donc pas apparaître dans un programme : ici, vous pouvez voir ce que gcc fait sur ces appels,mais d’autres compilateurs peuvent faire d’autres choix, et des versions ultérieures de gcc pourraient changer ces choix. De même des optimisations aggressives peuvent changer le comportement de ce code tout en respectant la norme du C.
Dans cet exercice, le résultat correspond à un certain ordre entre les différents arguments. Pour chaque résultat possible, identifiez la relation entre les différents arguments et écrivez-la la plus simplement possible. Vous devriez avoir trois cas très semblables.
Un prédicat dans Frama-C peut simplement être vu comme une macro logique. Son principal intérêt est de rendre les spécifications lisibles (notamment en n’écrivant qu’une seule fois des sous-formules complexes et en leur donnant un nom explicite).
Exemple : /*@ predicate inInterval(integer a, integer b, integer c) = a >= b && a <= c;*/ est un prédicat disant que a est inclus dans l’intervalle [b,c]. inInterval(1,0,3) est vrai, alors que inInterval(5,1,4) est faux.
Note: les prédicat peuvent avoir des arguments de n’importe quel type existant. Nous verrons plus d’exemples par la suite.
Les comportements permettent de spécifier des contrats de fonctions valides uniquement sur certaines plages de données. Cela permet essentiellement de rendre lisible des contrats de fonctions dont le comportement est très différent entre plusieurs cas.
Concrètement, un comportement est d’abord nommé avec la clause behavior toto:, puis on peut lui mettre une ou plusieurs clause assumes F; (ou F est une formule logique) qui désignent le domaine du comportement, et ensuite de clauses requires et ensures qui ont le même rôle que dans un contrat classique.
On peut enfin ajouter, dans le cas d’un contrat avec comportements, deux clauses permettant de vérifier que le contrat est bien formé : disjoint behaviors qui est vraie si les domaines comportements sont disjoints, et complete behaviors qui est vraie si les comportements couvrent tous les cas possibles des données. Ces deux clauses peuvent être appelées sur certains comportements seulement (ex. disjoint behaviors A,B,C; sera vraie si les comportement A, B et C sont disjoints).
Les deux techniques présentées dans cet exercice sont simplement des syntaxes alternatives de ce que l’on saurait déjà écrire, de manière à le rendre plus lisible. Cela ne devrait pas changer le comportement des solveurs, et est une bonne pratique à encourager. Mais en pratique, il y a des fois des blagues dans les preuves avec cela, et tout n’est pas aussi simple que ça à exprimer sous cette forme. Il ne faut donc pas absolument bannir les implications, il vaut mieux réserver les comportements pour de véritables comportements disjoint de la fonction. À noter également qu’on ne peut pas mettre des comportements dans des comportements.
This document was translated from LATEX by HEVEA.