-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path6-Revue_litterature.tex
1778 lines (1513 loc) · 88.3 KB
/
6-Revue_litterature.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\Chapter{REVUE DE LITTÉRATURE}\label{sec:RevLitt}
Le \emph{model-checking} rassemble des techniques permettant de vérifier
et valider des systèmes. Historiquement, le model-checking a
tout d'abord été utilisé pour la vérification de composants
électroniques. Les techniques ont ensuite été adaptées afin de permettre
la validation de programmes. On parle alors de model-checking logiciel.
Nous allons par la suite nous intéresser à celui-ci uniquement.
Le principe du model-checking est de mener une recherche
exhaustive à travers une modélisation du système plutôt que de
construire une preuve formelle de validité. Alors qu'une telle preuve
peut demander une certaine part d'intuition, le model-checking permet
une approche plus systématique, qui peut être plus facilement
automatisée.
De nombreux algorithmes de model-checking ont été développés en fonction
des propriétés à vérifier sur le système, du formalisme dans lequel est
décrit le système et de la complexité de celui-ci.
Dans ce chapitre, nous présentons les principales techniques de
model-checking logiciel et nous identifions leurs avantages et
inconvénients respectifs.
Nous identifions les outils les plus performants implémentant chacune de ces
techniques, à l'heure actuelle. Nous restreignons cependant cette étude aux
outils qui nous intéresseront pour la suite de ce rapport. Ces outils sont donc
susceptibles d'être utilisés en arrière-plan (backend) pour la vérification des
codes sources considérés dans ce mémoire. Nous ne considérerons donc que
des outils capables de vérifier des programmes concurrents en C, suivant le
modèle d'exécution défini par la norme POSIX (pThread).
\textbf{Structure :} Dans la section~\ref{sec:model-checking-logiciel}, nous
définissons le model-checking logiciel ainsi que les formalismes associés.
Dans la section~\ref{sec:specification}, nous présentons
les types de propriétés des programmes vérifiables grâce au model-checking et
les formalismes utilisés pour les exprimer et les représenter formellement.
Enfin, dans la section~\ref{sec:techniques-et-outils-de-model-checking}, nous
présentons différentes techniques utilisées pour le model-checking de programmes
multi thread ainsi que les outils qui les implémentent.
\section{Model-checking logiciel}\label{sec:model-checking-logiciel}
Le model-checking permet de prouver qu'un modèle d'un système vérifie une
spécification. Avant de nous intéresser davantage aux différentes techniques de
model-checking, nous allons présenter les notions de modèle et de spécification.
Nous définissions aussi un certain nombre de notions et termes techniques liés à
cette problématique.
\subsection{Modèle}
Le point caractéristique du model-checking est l'utilisation de
\emph{modèles} des systèmes à vérifier. Un modèle est une représentation
abstraite du système. Au lieu de vérifier un système directement, les techniques
de model-checking s'appliquent sur le modèle du système. On considère un
système comme correct lorsque son modèle ne contient pas d'erreurs.
L'origine des modèles est liée au domaine d'application initial du
model-checking : la vérification du design de composants électroniques. Il
était nécessaire de représenter ces derniers de manière abstraite afin de les
vérifier, ce qui a donné naissance à la notion de modèles.
L'utilisation d'un modèle permet de simplifier les tâches de vérification : un
modèle bien conçu capture le fonctionnement du système en retirant les détails
n'ayant pas d'impact sur les propriétés à vérifier. Cette version simplifiée du
système permet ainsi de réaliser des tâches de vérification complexes, qui ne
pourraient aboutir autrement. Cependant, le modèle doit être suffisamment fidèle
pour préserver les propriétés à vérifier. Un modèle trop simple pourrait
provoquer l'apparition de faux positifs ou pire, ne pas contenir une erreur
pourtant bien présente dans le système.
La phase de modélisation est donc une phase délicate et propice aux erreurs. Il
est nécessaire de bâtir un équilibre entre l'abstraction du système et le
respect de son fonctionnement. Cet équilibre dépend des propriétés que l'on
souhaite vérifier. La modélisation demande donc une bonne connaissance
du système et des outils de model-checking utilisés. Les tendances actuelles
visent à réduire l'intervention humaine dans la phase de modélisation en
automatisant celle-ci.
\subsection{Système de transitions et trace d'exécution}
Un modèle prend souvent la forme d'un système de transitions.
\paragraph{Système de transitions}
Formellement, un système de transitions est un triplet \((S, s_0, \rightarrow)\),
avec :
\begin{itemize}
\item
\(S\) l'ensemble des états du système ;
\item
\(s_0 \in S\) l'état initial du système ;
\item
\(\rightarrow \subset S \times S\) la relation de transitions du
système ;
\end{itemize}
Intuitivement, les états d'un système de transitions représentent un
statut possible du système, tandis que les transitions représentent les
actions qui peuvent le faire évoluer.
\paragraph{Trace d'exécution}
Une trace d'exécution est un chemin dans le système de transition représentant
le modèle, possiblement infini, dont le premier état est l'état initial du
système de transition. Une trace d'exécution est donc formée d'une suite \(s_0,
s_1, .. \in S\) d'états, avec \(s_0\) l'état initial, et d'une suite \(t_0, t_1,
... \in \rightarrow\) de transitions telles que \(\forall i, t_i = (s_i,
s_{i+1})\).
Le model-checking consiste à examiner l'ensemble des traces
d'exécution du modèle afin de vérifier si elles respectent la spécification.
\subsection{Vocabulaire technique}
Nous allons définir ici un certain nombre de termes liés au model-checking ou
aux méthodes formelles de manière générale, que nous réutiliserons par la suite.
\paragraph{Faux positif / Faux négatif}
Lorsqu'un outil de vérification indique une erreur dans un système alors que ce
dernier est en fait correct, on parle de \emph{faux positif}. À l'inverse, si un
outil reporte un système comme correct alors qu'il contient une erreur, on parle
de \emph{faux négatif}.
\paragraph{Complet / Correct}
Un outil ou une technique de vérification est complet s’il est conçu de sorte à
ne jamais faire de faux négatif : si une erreur existe dans un système, elle
sera toujours signalée.
Un outil ou une technique de vérification est correct s’il est conçu de sorte à
ne jamais faire de faux positif : si une erreur est signalée dans un système,
elle est réellement présente dans ce système.
Concevoir un outil de model-checking logiciel à la fois complet et correct est
extrêmement difficile, voire impossible, dans la plupart des cas. En effet, le
théorème de Rice indique que toute propriété sémantique non triviale d'un
programme est indécidable. Une technique de vérification ne respecte donc généralement
qu'une seule de ces caractéristiques, ou aucune des deux.
\subsection{Le cas du model-checking logiciel}
Le model-checking logiciel, ou \emph{software model-checking}, est un cas
particulier de model-checking. Le système à vérifier est un programme, et le
modèle va être extrait automatiquement à partir de son code source.
On automatise ainsi la conception du modèle, ce qui présente les avantages
suivants :
\begin{itemize}
\item
on réduit le risque d'erreur lors de la modélisation du système,
en supprimant l'intervention humaine ;
\item
on automatise davantage le procédé de vérification, ce qui le rend
plus facilement utilisable en pratique.
\end{itemize}
Cependant, le code doit encore être compilé afin d'obtenir le système,
ce qui constitue une encore une certaine distance entre le modèle et le
système. Afin de réduire cette distance, certains
model-checkers logiciels se basent sur des représentations intermédiaires
plus proches de l'assembleur, comme la représentation intermédiaire de
LLVM\footnote{L'utilisation de cette représentation est aussi expliquée
par des raisons de compatibilité. De nombreux langages, dont C, C++ et
C\# peuvent en effet être compilés vers la représentation
intermédiaire de LLVM, un model-checker utilisant cette dernière est
alors compatible avec tous ces langages.}.
La programmation impérative consiste à considérer un programme comme un état que
l'on fait évoluer à l'aide d'instructions. L'état du programme est
principalement défini par l'état de la mémoire du programme.
On peut alors modéliser un programme par un système de transition. L'ensemble
des états du système de transition est une partition de l'ensemble des états du
programme. Les états du système de transition sont donc caractérisés par :
\begin{itemize}
\item
la configuration du tas du programme ;
\item
la valeur des variables globales du programme ;
\item
la configuration de la pile de chaque thread ;
\item
la valeur du pointeur d'instruction de chaque thread.
\end{itemize}
Les instructions permettant de faire évoluer l'état du programme sont
représentées par des transitions entre les états de son modèle. Une transition
représente plus précisément l'action d'une instruction atomique du programme sur
les trois zones mémoires citées précédemment. Les transitions peuvent aussi
dépendre de facteurs externes, comme les entrées du programme.
Dans le cadre du model-checking logiciel, deux hypothèses sont fréquemment
faites sur le système :
\begin{itemize}
\item
la consistance séquentielle. On considère alors que les écritures et
les lectures mémoires ont lieu dans l'ordre où les instructions sont
rencontrées dans une trace d'exécution. En pratique, cette hypothèse
n'est pas toujours vérifiée. Les compilateurs et les processeurs
peuvent modifier l'ordre de ces opérations pour optimiser un
programme. On parle alors de modèle mémoire \emph{faible}.
\item
la sémantique d'entrelacement. Dans le cas d'un système multi thread,
on considère que les traces d'exécution possibles sont produites par
un entrelacement des instructions atomiques de chaque thread. Cette
hypothèse n'est pas valide si le programme est exécuté par plusieurs
processeurs : il est alors possible d'exécuter plusieurs instructions
simultanément, ce qui peut produire des traces d'exécution
supplémentaires.
\end{itemize}
\paragraph{Exemple}
Considérons le programme suivant, formé de deux threads.
\noindent\begin{minipage}{.45\textwidth}
\begin{lstlisting}[caption=Thread 1, frame=single]
a <- 1
a <- 2
\end{lstlisting}
\end{minipage}\hfill
\begin{minipage}{.45\textwidth}
\begin{lstlisting}[caption=Thread 2,frame=tlrb]
c <- 1
d <- a + c
\end{lstlisting}
\end{minipage}
Ce programme contient trois variables globales, \texttt{a}, \texttt{c} et
\texttt{d}, et deux threads. Les états du système sont donc définis par les
valeurs de ces variables et la position des pointeurs d'instruction de chaque
thread. On peut modéliser ce programme par le système de transitions en Figure
\ref{fig:model_example}. L'étiquette de chaque état représente la valeur des
variables. Les transitions représentent l'exécution d'une instruction. On
remarque l'existence de nombreuses traces d'exécution, pouvant mener à trois
résultats différents.
\begin{figure}
\begin{center}
\includegraphics[width=\textwidth]{model_example}
\end{center}
\caption{Système de transition modélisant un programme simple.}
\label{fig:model_example}
\end{figure}
\subsection{Non-déterminisme}
Un système est rarement complètement déterministe. De nombreux facteurs (les
données en entrée du système, l'instant où un évènement a lieu, la réussite des
allocations mémoires, la concurrence) tendent généralement à rendre l'évolution
d'un système non déterministe.
Face à un choix non déterministe, les techniques de model-checking doivent
explorer toutes les alternatives possibles.
Lors de la modélisation, certaines sources de non-déterminisme sont ignorées.
Elles sont alors remplacées par une exécution déterministe.
C'est généralement le cas pour les appels système ayant la possibilité
d'échouer (les allocations de mémoire, par exemple).
Pour représenter les évènements et les paramètres extérieurs du programme,
une fonction simulant le non-déterminisme est généralement mise à disposition
de l'utilisateur par les outils de model-checking.
\subsection{Explosion combinatoire}
Le nombre d'états d'un programme augmente exponentiellement selon de nombreux
paramètres (nombre de variables du programme, taille des types de données, degré
de concurrence\dots). Il peut éventuellement être infini si des appels de
fonctions ou de l'allocation dynamique de mémoire entrent en jeu. Cependant, afin
de vérifier un système, un model-checker doit explorer l'ensemble de ces états.
Cette tâche devient donc extrêmement coûteuse en temps et en mémoire quand le
nombre d'états devient trop important.
Ce problème, nommé l'explosion combinatoire, est la principale limite du
model-checking.
Le non-déterminisme est une des principales raisons de l'explosion combinatoire :
le nombre d'exécutions est exponentiel selon le nombre de choix non déterministe.
Cela explique les difficultés rencontrées par les techniques de model-checking
face aux programmes concurrents : l'ordonnancement entre les instructions est
non-déterministe. Il existe un nombre d'entrelacements exponentiel en fonction
du nombre d'instructions du programme.
Les différents algorithmes de model-checking utilisent tous des
techniques afin de limiter l'explosion combinatoire. Elle reste
cependant le plus grand obstacle rencontré par le model-checking et
limite le passage à l'échelle de la plupart des techniques.
\section{Formalismes de spécification}\label{sec:specification}
Afin de vérifier un système, il faut tout d'abord établir ce que signifie être
correct pour ce système. Un ingénieur responsable du design d'un système a
généralement une connaissance informelle de la manière dont le système doit se
comporter. Il est alors nécessaire de traduire cette connaissance d'une manière
non ambiguë et compréhensible par des outils. On réalise pour cela une
spécification du système.
Une spécification est constituée d'un ensemble de propriétés, qui représentent
des invariants logiques que le système doit respecter. Si l'un de ces invariants
est brisé, une erreur est présente : le système ne se comporte pas de la manière
attendue. Un outil de vérification peut alors inspecter un système afin de
vérifier si l'ensemble de la spécification est respecté.
Une spécification peut prendre de nombreux aspects : il existe des langages de
spécification (UML par exemple), elle peut être constituée de formules logiques
portant sur les variables du système ou prendre la forme d'un système de
transitions. Un langage de programmation constitue en lui même une spécification
bas niveau du comportement d'un programme.
Les propriétés d'une spécification se répartissent en plusieurs catégories. Une
propriété peut être un comportement généralement attendu pour le type de système
concerné. Par exemple, on attend généralement d'un programme qu'il ne contienne
pas de comportements indéterminés. Une propriété peut aussi être spécifique au
système et à son comportement. On peut par exemple spécifier qu'une variable ne
doit jamais dépasser un certain seuil.
\subsection{\texorpdfstring{Propriétés ``built-in''}{Propriétés built-in}}
Certaines propriétés sont fortement --- si ce n'est toujours --- désirables
pour une catégorie de systèmes. Un model-checker ciblant cette catégorie
de systèmes peut alors implémenter la vérification de ces propriétés
nativement, sans qu'il soit nécessaire de les spécifier. On parle alors
de propriétés \emph{built-in}.
Dans le cas du model-checking logiciel, des propriétés built-in
classiques sont la vérification de la validité des pointeurs et des
opérations arithmétiques, l'initialisation des variables et, de manière
plus générale, l'absence de tout comportement ne respectant pas la norme
du langage.
Dans le cas de programmes concurrents, on cherche généralement à s'assurer de
l'absence de dreadlocks (lorsque tous les threads du programme sont bloqués par une
condition, et que le système ne peut plus évoluer) et de data-races (plusieurs
accès simultanés à une même adresse mémoire, dont au moins un en écriture).
Les propriétés built-in sont souvent des propriétés qu'il serait difficile à
l'utilisateur de spécifier, parce qu'elles impliquent la plupart des variables
du système.
Pour vérifier des propriétés plus spécifiques au système (des propriétés
fonctionnelles par exemple, qui portent sur le résultat que le système doit
produire) il est cependant nécessaire de permettre à l'utilisateur d'exprimer
lui-même la spécification.
\subsection{Assertions}
Dans un article de son blog, John Regehr aborde le sujet des
assertions\cite{assertion_regehr}.
Il reprend en particulier la définition suivante :
\begin{quotation}
\selectlanguage{english}
An assertion is a Boolean expression at a specific point in a program which will
be true unless there is a bug in the program.
\hfill \cite{assertion_regehr}
\end{quotation}
%% \begin{quotation}
%% ASSERT(expr)
%% Asserts that an expression is true. The expression may or may not be evaluated.
%% If the expression is true, execution continues normally.
%% If the expression is false, what happens is undefined.
%% \hfill \cite{assertion_regehr}
%% \end{quotation}
\selectlanguage{french}
Cette définition présente les assertions comme un mécanisme de spécification
exécutable. Si une assertion n'est pas respectée, une erreur est présente dans
le programme. Les assertions sont présentes dans la plupart des langages de
programmation. Elles ont été naturellement reprises par les méthodes de
vérification formelle, dont le model-checking logiciel.
Les assertions permettent de vérifier des propriétés d'accessibilité. Ces
propriétés consistent à vérifier s’il est possible d'atteindre un état donné du
modèle. Cet état est appelé un état d'erreur. On peut aussi considérer
l'accessibilité d'un ensemble d'états d'erreurs.
Les propriétés d'accessibilité sont des propriétés relativement simples :
déterminer si un état est un état d'erreur ne dépend que de cet état. Un
programme contient donc une erreur dès qu'il est possible d'atteindre un état
d'erreur, indépendamment des états et des transitions empruntées au cours de
l'exécution. Une propriété d'accessibilité peut ainsi être déterminée par une
exploration (en largeur par exemple) du système.
Dans un programme, une assertion prend la forme d'une instruction telle que :\\
\texttt{assert(condition);}.
Si elle est exécutée et que la condition est évaluée à vrai, alors l'exécution
du programme continue normalement.
Sinon, le programme a atteint un état d'erreur. Son comportement est alors
indéterminé, bien que le comportement le plus fréquent soit de stopper son
exécution.
Plus formellement, l'assertion \texttt{assert(c)} désigne comme étant des
états d'erreur tous les états tels que :
\begin{itemize}
\item
un des pointeurs d'instruction du programme pointe sur l'assertion
\item
l'expression \texttt{c} s'évalue à faux
\end{itemize}
Dans le code suivant, une assertion permet de spécifier que la variable
\texttt{b} doit être non nulle. Dans le cas contraire, une erreur
arithmétique (division par zéro) pourrait avoir lieu à la ligne suivante.
\begin{lstlisting}[language=C, frame=single]
int int_div(int a, int b) {
assert(b != 0);
return a / b;
}
\end{lstlisting}
Les assertions sont très utilisées en raison de leur simplicité.
Elles peuvent de plus être placées en tant que spécification pour un outil de
vérification, ou permettre de signaler une erreur dans une version exécutable
du programme (il ne faut cependant pas les confondre avec un mécanisme de
gestion d'erreurs).
Cependant, le pouvoir d'expression des assertions est limité. Toutes les
propriétés d'accessibilité ne peuvent pas être exprimées par une assertion : il
n'est par exemple pas possible d'utiliser dans la condition d'une assertion des
variables hors du contexte courant. Il n'est pas non plus possible d'exprimer un
problème d'exclusion mutuelle : il faudrait pour cela référer à la position des
autres pointeurs d'exécution du code, ce qui n'est pas possible à l'aide d'une
assertion.
\subsection{Logique temporelle}
Pour exprimer des propriétés plus complexes et en particulier des propriétés qui
portent sur l'ensemble d'une trace d'exécution et l'ordre d'apparition de
certains évènements, il est plus approprié d'utiliser une logique temporelle.
Supposons que notre système soit le code d'un distributeur automatique.
On voudrait s'assurer que le distributeur ne délivre jamais le produit
avant que le client ait payé, ce qui va revenir dans le code à vérifier
que la fonction \texttt{livrer\_produit} (qui commanderait au système
de donner au client l'objet commandé) n'est jamais appelée avant la
fonction \texttt{accepter\_paiement} (qui validerait que le paiement a
été correctement effectué), au cours d'une transaction. Des assertions
permettent de déterminer si chacune de ces fonctions est atteinte, mais
sans introduire des variables auxiliaires, il n'est pas possible de
déterminer l'ordre des appels.
Les logiques temporelles permettent d'exprimer ce type de propriétés. On
peut ainsi spécifier, pour un programme, des propriétés sur la
succession des états d'une trace d'exécution.
Ci-dessous sont listés quelques-un des schémas de propriétés les plus
courantes. Davantage sont présenté par \citep{LTL_scheme}. Soit \(p\) une
propriété portant sur les états du système.
\begin{itemize}
\item
propriété de sûreté : tous les états atteints pendant l'exécution
vérifient la propriété \(p\).
\item
propriété d'accessibilité : en un temps fini, un état vérifiant la
propriété \(p\) est atteint.
\item
propriété d'équité : on atteindra infiniment souvent un état vérifiant la
propriété \(p\).
\end{itemize}
Les logiques temporelles les plus utilisées sont : \acl{LTL} (\ac{LTL})
et \ac{CTL}.
\ac{LTL} et \ac{CTL} différent principalement par leur vision de l'ensemble des
traces d'exécution. \ac{LTL} considère chaque trace indépendamment. Un
système est valide par rapport à une propriété \ac{LTL} si toutes ses traces
d'exécution respectent la propriété. \ac{CTL} considère l'ensemble des traces
comme un arbre et permet de quantifier universellement
ou existentiellement sur les successeurs de chaque nœud.
Les pouvoirs d'expressivités de \ac{LTL} et \ac{CTL} ne sont ni équivalents, ni même
comparables. En effet, \ac{LTL} ne permet pas de quantifier existentiellement :
une formule \ac{CTL} utilisant une quantification existentielle n'a donc pas
toujours d'équivalent en \ac{LTL}. La réciproque peut être présentée à l'aide de
la propriété \ac{LTL} \(F (p \land X p)\) (pour toutes les traces, on atteint un
état vérifiant \(p\) et dont le successeur vérifie \(p\)) n'a pas d'équivalent
dans \ac{CTL}. La formule \ac{CTL} \(AF (p \land AX p)\) (quelque soit le chemin
emprunté, on atteint un état vérifiant \(p\) et dont tous les successeurs
vérifient \(p\)) pourrait sembler un bon candidat, mais la Figure
\ref{LTL_vs_CTL} présente un modèle vérifiant \(F (p \land X p)\) mais ne
vérifiant pas \(AF (p \land AX p)\).
\begin{figure}
\begin{center}
\includegraphics[width=0.4\textwidth]{LTL_CTL_non_equivalent.png}
\caption{Modèle vérifiant \(F (p \land X p)\) mais pas \(AF (p \land AX p)\).}
\label{LTL_vs_CTL}
\end{center}
\end{figure}
\ac{CTL} est considérée comme plus difficile à comprendre par les ingénieurs,
plus habitués à penser à une unique exécution linéaire plutôt qu'à un arbre
d'exécution\cite{RCTL_formulas}. \ac{CTL} est par conséquent moins utilisée que
\ac{LTL} dans le cadre du model-checking logiciel. Par la suite, nous allons
donc nous concentrer sur \ac{LTL} uniquement.
\subsection{LTL : Logique Temporelle Linéaire}
La définition de \ac{LTL}\cite{pnueli_LTL} ajoute deux opérateurs temporels à la
logique classique, \(next\) (\(X\)) et \(until\) (\(U\)). La syntaxe d'une
formule \ac{LTL} est définie de la manière suivante, pour \(\phi\) et \(\psi\)
deux formules \ac{LTL} :
\[
\phi, \psi := \text{true }| \text{ false } | \text{ p } |
\phi \land \psi | \lnot \phi | X \phi | \psi U \phi
\]
\(p\) est une proposition sur l'état du système. On nommera par la suite
ces propriétés des \emph{propositions atomiques}. On identifiera aussi
une proposition atomique avec sa fonction d'évaluation, c'est-à-dire la
fonction qui indique si un état du système vérifie la propriété ou non.
Étant donnée une trace d'exécution infinie \(s = (s_0, s_1, ...)\), \ac{LTL} a
la sémantique suivante :
\[
\begin{aligned}
s \models p & \equiv s_0 \models p \\
s \models X \phi & \equiv (s_1, s_2, \dots) \models \phi \\
s \models \phi U \psi & \equiv \exists k, (s_k, s_{k+1}, \dots) \models \psi
\land \forall i <= k, (s_i, s_{i+1}, \dots) \models \phi \\
\end{aligned}
\]
\(\lnot\), \(\land\), \(true\) et \(false\) s'interprètent de la manière
usuelle.
Une trace d'exécution est un modèle d'une proposition atomique si son premier
état est un modèle de la proposition atomique.
L'opérateur \(next\) signifie donc que la propriété \ac{LTL} passée en
paramètre doit être valide sur la trace privée de son premier état.
L'opérateur \(until\) signifie que la première propriété passée en
paramètre doit être vérifiée par tout les états de la trace jusqu'à ce qu'un
état vérifie la seconde formule.
À partir de ces opérateurs base, on définit les opérateurs \(\lor\),
\(\implies\), \dots de la manière classique. On définit aussi les
opérateurs temporels \(always\) (\(G\)), signifiant qu'une propriété est
vraie pour tous les états d'une trace et \(finally\) (\(F\)), signifiant
qu'un état vérifiant une propriété est atteint dans le futur.
\[
\begin{aligned}
F p & \equiv \text{true} U p \\
G p & \equiv \lnot F (\lnot p)\\
\end{aligned}
\]
Enfin, on définit un système comme étant valide par rapport à une
formule \ac{LTL} si toutes les exécutions de ce système sont des modèles de
la formule.
\subsection{Automates de Büchi}
Vérifier si un système respecte une proposition \ac{LTL} demande d'être capable de
manipuler efficacement ces dernières. On utilise pour cela les automates de
Büchi. Ils permettent de représenter une propriété \ac{LTL}. Sous cette forme, elles
sont plus simples à manipuler pour un model-checker. Toute formule \ac{LTL} peut être
représentée par un automate de Büchi. Il existe des algorithmes pour construire
efficacement et automatiquement cet automate\citep{ltl2ba}.
Afin de vérifier si un système respecte une propriété \ac{LTL}, une méthode classique
est de construire un automate de Büchi qui représente la négation de cette
formule \ac{LTL} et d'explorer la composition entre cet automate et le système de
transitions modélisant le système. Un chemin acceptant représente alors une
exécution du système qui viole la propriété.
\paragraph{Automates de Büchi}
Un automate de Büchi est un automate qui accepte un langage de mots
infinis. Formellement, un automate de Büchi est un quintuplé
\(B = (S, \Sigma, I, \delta, F)\), avec :
\begin{itemize}
\item
\(S\) un ensemble d'états ;
\item
\(\Sigma\) un alphabet ;
\item
\(I \subset S\) est un ensemble d'états initiaux ;
\item
\(\delta \subset (S, \Sigma, S)\) est la relation de transition ;
\item
\(F \subset S\) est l'ensemble des états finaux.
\end{itemize}
Un calcul (les mots chemin ou trace sont aussi utilisés) dans \(B\) est une
suite infinie de transitions consécutives \(c \in \delta^\omega\), dont l'état
de départ est un état initial :
\[
c = (s_0, a_0, s_1)(s_1, a_1, s_2)\dots(s_n, a_n, s_{n+1})\dots
\]
L'étiquette de ce chemin est le mot infini \(a = (a_0, a_1, \dots, a_n) \in
\Sigma^\omega\). Le chemin \(c\) est réussi si et seulement s’il passe une
infinité de fois par un état final de \(B\).
L'automate \(B\) accepte un mot \(a\) si et seulement s’il existe un calcul réussi dans \(B\) ayant le mot \(a\) pour étiquette.
Pour représenter une formule \ac{LTL}, on prend \(\Sigma = 2^P\), avec \(P\)
l'ensemble des propositions atomiques de la formule. Une lettre de
l'alphabet représente ainsi une configuration des propositions atomiques
du système.
\paragraph{Exemples}
La figure \ref{fig:buchi_example} présente les automates de Büchi pour trois propriétés
\ac{LTL} simples : \(Gp\), \(F(p \lor q)\) et \(G(Fp)\). Seule la partie accessible des automates
est représentée, et les transitions ayant les mêmes sources et destinations sont fusionnées,
leurs étiquettes étant remplacées par une garde sous la forme d'une expression logique.
\begin{figure}
\begin{center}
\includegraphics[height=.17\textheight]{buchi_1.png}
\includegraphics[height=.3\textheight]{buchi_3.png}
\includegraphics[height=.3\textheight]{buchi_2.png}
\end{center}
\caption{Automates de Büchi pour les formules LTL $G p$, $F(p \lor q)$ et
$GF p$.}
\label{fig:buchi_example}
\end{figure}
\paragraph{Automate produit}
Soient un automate de Büchi \(B = (S_B, \Sigma, I_B, \delta, F_B)\) représentant
une propriété \ac{LTL} \(\phi\) et un système de transitions \(T = (S_T, \rightarrow,
I_B)\) modélisant un système.
L'automate produit de \(B\) et \(T\) est définit comme étant \(P = (S_P, \Sigma,
I_P, \delta_P, F_P)\), avec :
\begin{itemize}
\item
\(S_P = S_B \times S_T\)
\item
\(I_P = I_B \times I_T\)
\item
\(F_P = F_B \times S_T\)
\item
\(((p, q), s, (p', q')) \in \delta_P\) si et seulement si :
\begin{itemize}
\item
\((p, p') \in \rightarrow\)
\item
\((q, s, q') \in \lambda\)
\item
\(p \models s\)
\end{itemize}
\end{itemize}
Un mot est accepté par l'automate produit si et seulement s’il représente
une exécution valide dans le modèle du système et qu'il appartient au langage
de l'automate de Büchi.
L'automate produit \(P\) reconnaît donc exactement le langage des exécutions du
modèle \(T\) qui vérifient la propriété \ac{LTL} représentée par l'automate de
Büchi \(B\).
\paragraph{Vérifier une propriété LTL}
Vérifier si le modèle \(T\) respecte la propriété \(\phi\), revient à calculer
si le langage des exécutions valide de \(T\) est inclus dans le langage de
l'automate de Büchi \(B\). Cependant, vérifier une inclusion est une opération
complexe. Il est plus simple de calculer si langage est vide ou non. On va donc
reformuler le problème : on va chercher à déterminer si le langage \(L\) des
exécutions valides dans \(T\) et ne respectant \emph{pas} \(\phi\) est vide.
Si c'est le cas, le système est correct. Sinon, une erreur est présente et les
éléments de \(L\) représentent des contre-exemples.
Pour calculer le langage \(L\), on va construire le produit entre le système de
transitions du modèle et l'automate de Büchi représentant la \emph{négation} de
\(\phi\). Le langage de l'automate produit est alors constitué des exécutions valides du
modèle qui ne respectent pas \(\phi\), soit \(L\).
Il suffit alors d'explorer l'automate produit. S’il est possible, à partir
de l'état initial, d'atteindre un cycle contenant au moins un état final, alors,
\(L\) n'est pas vide : il contient au moins le mot composé par l'étiquette du
chemin allant jusqu'au cycle et d'une infinité de répétitions de l'étiquette du
cycle. Ce mot correspond à une exécution du système qui viole la propriété \(\phi\),
et pourra servir de contre-exemple à l'utilisateur.
L'encodage des propriétés \ac{LTL} à l'aide des automates de Büchi permet ainsi de
vérifier des propriétés complexes sur les modèles. Cependant, la taille d'un
automate de Büchi augmente exponentiellement avec la profondeur de la formule
qu'il représente. Dans le cas de formules de grande taille, il peut donc devenir
problématique de générer l'automate. Sa taille et son aspect non déterministe
viennent renforcer le problème d'explosion combinatoire déjà rencontré par les
techniques de model-checking.
\section{Techniques et outils de model-checking}
\label{sec:techniques-et-outils-de-model-checking}
Différents algorithmes sont utilisés par les outils de model-checking
afin d'explorer l'ensemble des états d'un modèle. Afin de lutter contre
l'explosion combinatoire, ils établissent des compromis au niveau de la
précision de la vérification et du type de propriétés qu'ils prennent en charge.
Leurs performances dépendent aussi fortement de la
structure du programme : les boucles, le non-déterminisme ou la gestion
de la concurrence sont plus ou moins bien supportés selon
les algorithmes.
À l'exception des techniques de séquentialisation, ces algorithmes ont été
initialement appliqués à des programmes séquentiels. Le support des programmes
concurrents est venu ensuite\footnote{Les algorithmes inspirés du model-checking
de composants électroniques supportent des modèles concurrents depuis
longtemps. Cependant, le support de la concurrence pour le model-checking
logiciel a été ralenti par l'explosion combinatoire.}.
Ce dernier n'est pas forcément complexe, d'un point de vue théorique : un
programme multi thread peut s'exprimer comme un système de transitions
non déterministe. Les model-checkers sont capables de vérifier des modèles
non déterministes --- ils sont utilisés pour simuler les entrées possibles d'un
système par exemple --- les algorithmes de model-checking peuvent donc
théoriquement être étendus.
Le problème est bien plus complexe en pratique, en raison des contraintes de
temps et de mémoire que rencontrent les model-checkers. En raison de l'explosion
combinatoire provoquée par le nombre exponentiel d'entrelacements possibles
entre les threads, il est difficile de concevoir un algorithme performant.
Nous présentons dans cette partie les principaux algorithmes utilisés dans le
cadre du model-checking de logiciel multi thread, ainsi que certains des outils
qui les implémentent. Pour chaque algorithme, nous avons choisi les outils qui
nous ont parus les plus aboutis et ceux qui présentaient une approche originale.
Pour chaque algorithme et outil, nous tentons de mettre en valeur ses points
forts, ses faiblesses et ses spécificités.
\subsection{Model-checking explicite}
Le model-checking explicite consiste à énumérer individuellement les
états accessibles du modèle afin de les explorer. Le modèle prend la
forme d'un système de transitions et son exploration est réalisée à
l'aide d'algorithmes de graphes (exploration en largeur ou en
profondeur\dots).
Les algorithmes de model-checking explicite modélisent le programme par un
système de transitions. Ils construisent et explorent ce système de transitions
à la volée : partant de l'état de départ, les successeurs sont déterminés à
partir des instructions du programme. Ils sont alors visités à leur tour, leurs
successeurs sont calculés et ainsi de suite.
Pour ne pas explorer plusieurs fois un même état (l'exploration entrerait alors
dans un cycle infini), il est nécessaire de stocker les états du chemin
emprunté.
Le model-checking explicite est extrêmement vulnérable à l'explosion
combinatoire. Les états accessibles du système sont examinés individuellement.
S’ils sont en nombre infinis, une analyse peut ne pas terminer. Le stockage des
états explorés peut aussi devenir problématique. Plusieurs techniques existent
pour réduire l'impact de l'explosion combinatoire.
\paragraph{Stateless Model-checking}
Certains outils choisissent de ne pas maintenir la liste des états
visités afin de ne pas être limités par l'utilisation de la mémoire. On
parle alors de \emph{stateless model-checking}. Ces outils ne sont pas
capables de détecter un cycle dans le programme à analyser. Il est donc
nécessaire que toutes les exécutions du programme à analyser terminent.
\paragraph{State Hashing}
Le \emph{state hashing} consiste à ne conserver qu'une valeur de hachage des
états visités, et non pas l'état complet. L'utilisation de la mémoire est ainsi
réduite, et les performances peuvent être améliorées par l'utilisation de
structures de données capables de rechercher un élément de manière efficace
(ensemble ordonné, \dots). Cependant, il est possible (bien que très peu
probable) que deux états aient la même valeur de hachage. Ces états sont alors
considérés comme égaux, ce qui peut mener à un résultat faux de l'analyse.
Le state hashing rend donc les model-checkers incomplets.
\paragraph{Runtime model-checking}
Certains outils explorent l'espace d'états en se basant sur des
exécutions réelles du programme. Le modèle est alors le programme lui-même.
Son exécution est contrôlée par le model-checker. Ce dernier contrôle le
non-déterminisme de chaque exécution. Cette approche permet de
bénéficier des performances réelles du programme lors de l'exploration,
cependant le backtracking est plus compliqué : les outils de runtime
model-checking relancent en général une exécution du programme à tester
depuis l'état initial chaque fois qu'il est nécessaire de revenir à un
état précédent. Il est aussi complexe de mémoriser les états explorés,
cette technique est donc souvent combinée avec le \emph{stateless model
checking}.
\paragraph{Réduction par ordre partiel}
Les techniques de réduction par ordre partiel visent à réduire le nombre de
chemins à explorer dans un système concurrent. Elles permettent de supprimer les
chemins équivalents par rapport aux propriétés à vérifier, souvent en supprimant
des changements de contextes entre des instructions indépendantes. Un cas simple
de réduction consiste à regrouper en un bloc atomique une série d'instructions
manipulant uniquement des variables locales. L'entrelacement de ces instructions
avec d'autres threads n'a pas d'impact sur leur résultat, on peut donc conserver
uniquement l'ordonnancement consistant à les exécuter sans changement de
contexte.
La plupart des variantes ci-dessus sont aussi utilisées par les autres
algorithmes que nous présentons par la suite, en particulier les
techniques de réduction par ordre partiel. Cependant, le model-checking
explicite est particulièrement dépendant de ces techniques afin de
gagner en performances.
La principale force du model-checking explicite est sa précision. Explorer
l'ensemble des états de manière explicite permet de vérifier la plupart des
propriétés, spécifiées à l'aide d'assertions ou par une formule \ac{LTL}. Cependant,
il est extrêmement dépendant des techniques de réduction et a plus de
difficultés face à des programmes de grande taille.
\subsubsection{Outils}
SPIN\cite{SPIN} est l'un des premiers projets de model-checking
explicite. Il permet de vérifier des propriétés de la logique temporelle
\ac{LTL} sur des modèles exprimés en \bsc{PROMELA}. SPIN supporte nativement
les modèles concurrents et implémente de nombreuses méthodes de réduction
(ordre partiel, bit state hashing\dots). SPIN ne supporte pas le langage
C nativement, cependant des travaux ont été menés afin de traduire C
vers \bsc{Promela}\cite{jiang_C_to_Promela}, ce qui permet à SPIN de
vérifier un programme en C de manière indirecte. Cependant, cette traduction
ne supporte que partiellement les manipulations de la mémoire (pointeurs,
allocation dynamique\dots).
Pancam\cite{Pancam} se base sur SPIN pour vérifier du bytecode
LLVM\footnote{le bytecode LLVM peut être produit à partir d'un code C à
l'aide d'un compilateur tel que CLANG ou GCC}. Pancam exécute le
bytecode LLVM dans une machine virtuelle et utilise SPIN comme un
moniteur d'exécution afin de générer les différents entrelacements à
explorer.
CHESS\cite{Chess} est un outil de runtime model-checking. Étant donné un
scénario de test, il permet d'explorer l'ensemble des entrelacements à l'aide
d'un ordonnanceur. Celui-ci a la particularité de ne pas autoriser un changement
de contexte en tout point du programme : un changement de contexte est possible
lorsque une primitive de synchronisation ou un point défini par l'utilisateur
sont atteints ou lorsqu'un thread est dans une situation de livelock (son
exécution boucle en attendant une action d'un autre thread). Il permet ainsi de
limiter l'explosion combinatoire due à la concurrence.
\texttt{inspect}\cite{inspect} vérifie du code C et C++
multi thread en se basant sur un algorithme de runtime model-checking
avec une exploration \emph{stateless}. \texttt{inspect} instrumente
le programme à vérifier par des instructions lui permettant de
communiquer avec un ordonnanceur, selon une architecture client/serveur.
Le programme est ensuite exécuté pour chaque combinaison d'entrelacements,
afin d'explorer l'ensemble des traces possibles.
Divine\cite{Divine_3_0} est un model-checker capable de vérifier des
propriétés \ac{LTL} et des propriétés d'accessibilités. Il se base sur un
langage interne, DVE. Il est capable de traiter du bytecode LLVM en le
traduisant vers DVE, ce qui lui permet de supporter des langages comme C
et C++. La particularité de Divine est de mettre en place une analyse
concurrente et distribuée afin d'améliorer ses performances. Il utilise
aussi des méthodes de réduction de l'espace d'état (compression de
chemins, réduction par ordre partiel).
On retrouve à travers ces outils le besoin de performance des outils de
model-checking explicite. Divine est implémenté dans une architecture
concurrente pour améliorer ses performances, alors que \texttt{inspect} et
Pancam se basent sur du runtime model-checking. SPIN, \texttt{inspect}, Pancam
et Divine sont tous capables de vérifier des violations d'assertions et des
propriétés built-in, comme les deadlocks et les data-races, cependant, seul
Divine et SPIN sont capables de vérifier des propriétés \ac{LTL}.
\texttt{inspect} nécessite un système fermé, et n'est pas capable de gérer le
non-déterminisme. Il est donc dépendant d'une suite de tests pour fixer les
paramètres du programme à vérifier. Divine est arrivé sixième de la catégorie
portant sur les programmes concurrents lors de l'édition de 2016 de la
compétition de vérification logicielle SV-COMP\citep{svcomp_2016_result} (les
autres outils présentés ci-dessus n'ont pas participé à ces compétitions).
\subsection{Model-checking symbolique}
Les algorithmes de model-checking symbolique manipulent des ensembles d'états
du modèle représentés de manière abstraite, plutôt que d'énumérer chaque état
individuellement. Il est ainsi possible de manipuler des ensembles
d'états importants ou infinis de manière efficace. Contrairement au
model-checking explicite, le model-checking symbolique n'est donc pas limité à
un modèle fini.
L'abstraction utilisée pour représenter les ensembles d'états est un élément
clef du model-checking symbolique. Cette représentation doit permettre de
réaliser les opérations classiques sur les ensembles (union, intersection,
comparaison) de manière efficace. Les abstractions suivantes sont les plus
utilisées :
\begin{itemize}
\item
les BDD (\emph{Binary Decision Diagrams}, Diagrammes de Décision Booléens) ;
\item
les formules de la logique propositionnelle ;
\item
automates finis.
\end{itemize}
Ces représentations permettent de définir des opérations efficaces sur les
ensembles. L'opération la plus complexe est généralement la comparaison de deux
ensembles, qui nécessite le choix d'une représentation canonique.
Cependant, la taille de la représentation d'un ensemble peut varier très
fortement selon le choix de la représentation canonique. Le défi du
model-checking symbolique est donc de construire et de maintenir efficacement
une représentation canonique de taille raisonnable.
\subsubsection{Représentation symbolique par des BDD}
Nous allons utiliser l'exemple des BDD pour illustrer les forces et les
défis rencontrés par les représentations symboliques.
Un BDD permet de représenter une fonction booléenne.
On va donc représenter un ensemble d'états par une fonction prenant
en paramètre un état (encodé par des variables booléennes) et indiquant s’il est dans l'ensemble représenté par la fonction ou non.
\paragraph{Encoder les états}
La première étape est d'encoder les états du système par des variables
booléennes. Une manière simple de le faire dans le cas d'un ensemble fini
d'états est de les numéroter, puis d'utiliser une variable booléenne pour chaque
chiffre de la représentation binaire (il faut donc \(log_2 n\) variables
booléennes pour \(n\) états).
Prenons l'exemple d'un système à huit états. En numérotant les états de 0 à 7 et
en prenant leur représentation binaire (sur trois bits), on obtient les
représentations suivantes : (\(000\), \(001\), \(010\), \dots, \(111\)).
Pour représenter l'ensemble composé des états \(110\) et \(111\), il suffit
de prendre sa fonction caractéristique, c'est-à-dire la fonction qui s'évalue à
vrai pour les éléments de l'ensemble et à faux sinon.
\begin{align}
f \colon \{0, 1\}^3 & \to \{0, 1\} \\
(x_1, x_2, x_3) & \mapsto
\begin{cases*}
1 & si \((x_1, x_2, x_3) \in \{(1,1,0), (1,1,1)\}\) \\
0 & sinon
\end{cases*}
\end{align}
Nous allons ensuite construire un BDD pour représenter cette fonction.
La construction d'un BDD repose sur la propriété suivante :
\paragraph{Décomposition des fonctions booléennes}
Soit \(k > 0\) et \(f: \mathbf{B}^k \to \mathbf{B}\) une
fonction booléenne. Alors il existe deux fonctions booléennes
\(g, h : \mathbf{B}^{k-1} \to \mathbf{B}\) telles que :
\[
f(x_1, ..., x_k) =
\begin{cases*}
g(x_2, ..., x_k) \text{ si $x_0$ est vrai}\\
h(x_2, ..., x_k) \text{ si $x_0$ est faux}
\end{cases*}
\]
Il est ainsi possible de définir une fonction booléenne par récursion
sur le nombre de ses variables. On va se servir de cette propriété pour
représenter la fonction booléenne par un arbre binaire de décision. On
fixe tout d'abord un ordre des variables (dans notre exemple, on prend
le bit de poids le plus fort en premier). On construit alors l'arbre
par induction :
\begin{itemize}
\item
si la fonction est constante, on la représente par une feuille
étiquetée par \(vrai\) ou \(faux\).
\item
sinon, on décompose \(f\) entre \(g\) et \(h\) selon la propriété. On
construit alors l'arbre dont le fils gauche est la représentation de
\(g\) et le fils droit celle de \(h\) (construites selon la même méthode).
On étiquette l'arrête vers le fils gauche par \(0\) et celle vers le fils
droit par \(1\).
\end{itemize}
Les étiquettes d'un chemin de la racine de l'arbre à une feuille représentent
alors une valuation des variables de la fonction. L'étiquette de la feuille
indique la valeur de la fonction pour cette valuation.
Pour notre exemple, nous obtenons l'arbre de la Figure \ref{fig:BDD_tree}.
\begin{figure}[ht!]
\begin{center}
\includegraphics[height=0.3\textheight]{BDD_tree.png}
\end{center}
\caption{Arbre binaire de la fonction \((x,y,z) \mapsto x \land y\).}
\label{fig:BDD_tree}
\end{figure}
La représentation par un arbre de décision contient \(2^{n+1} - 1\) nœuds pour
une fonction booléenne à \(n\) variables, on ne peut donc pas encore parler de
représentation efficace. Cependant, cet arbre de décision contient aussi
beaucoup de sous-arbres redondants. On va donc réduire cet arbre en un BDD en
fusionnant tous les sous-arbres identiques et en supprimant les états n'ayant
qu'un unique successeur. Dans notre exemple, on obtient alors le BDD de la
Figure \ref{fig:BDD_graph}.
\begin{figure}[h]
\begin{center}
\includegraphics[height=0.25\textheight]{BDD_graph.png}
\end{center}
\caption{BDD de la fonction \((x,y,z) \mapsto x \land y\).}
\label{fig:BDD_graph}