0% found this document useful (0 votes)
4 views18 pages

Paper Ok

Uploaded by

x0011100000
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views18 pages

Paper Ok

Uploaded by

x0011100000
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 18

Static Analysis by Abstract Interpretation of Sequential

and Multi-Thread Programs


Antoine Miné

To cite this version:


Antoine Miné. Static Analysis by Abstract Interpretation of Sequential and Multi-Thread Programs.
10th School of Modelling and Verifying Parallel Processes, Dec 2012, Marseille, France. �hal-00763076�

HAL Id: hal-00763076


https://inria.hal.science/hal-00763076v1
Submitted on 10 Dec 2012

HAL is a multi-disciplinary open access L’archive ouverte pluridisciplinaire HAL, est


archive for the deposit and dissemination of sci- destinée au dépôt et à la diffusion de documents
entific research documents, whether they are pub- scientifiques de niveau recherche, publiés ou non,
lished or not. The documents may come from émanant des établissements d’enseignement et de
teaching and research institutions in France or recherche français ou étrangers, des laboratoires
abroad, or from public or private research centers. publics ou privés.
Static Analysis by Abstract Interpretation of
Sequential and Multithreaded Programs

Antoine Miné?

CNRS & École Normale Supérieure


45, rue d’Ulm
75005 Paris, France
[email protected]

Abstract. In the realm of embedded critical systems, it is crucial to


guarantee the correctness of programs before they are deployed. Static
analyzers can help by detecting at compile-time potentially erroneous
program behaviors: they perform sound over-approximations to achieve
an efficient analysis while not missing any potential behavior. We discuss
the systematic design of such analyzers using abstract interpretation,
a general theory of semantic approximation. After recalling the classic
construction of static analyzers for sequential programs by abstraction of
the concrete trace semantics, we introduce abstractions to derive thread-
modular analyzers for multithreaded programs, borrowing ideas from
rely/guarantee proof methods. Finally, we present two static analyzer
tools, Astrée and AstréeA, that are used to check for run-time errors
in large sequential and multithreaded embedded industrial avionic C
applications.

1 Introduction

It is crucial to guarantee the correctness of programs before they are deployed,


especially in the realm of embedded critical systems, where software cannot be
corrected during missions and a single mistake can have dramatic consequences.
Testing, the most widespread verification method employed in industry, is very
efficient at catching errors, but it is costly and cannot, for efficiency reasons, test
all executions. Hence, testing can miss errors. This is especially true for parallel
and multithreaded programs: the huge number of possible thread interleavings
causes a combinatorial explosion of executions, while some errors only appear
in a tiny fraction of them (such as data-races). Formal methods, on the other
hand, can provide rigorous guarantees that all the executions are correct.
We consider here static analyses, able to inspect program sources in order
to find defects. We only consider semantic analyses, that are based on a mathe-
matical notion of program behaviors, as opposed to syntax-based style checkers.
?
This work was partially supported by the INRIA project “Abstraction” common to
CNRS and ENS in France, and by the project ANR-11-INSE-014 from the French
Agence nationale de la recherche.
Unlike proof methods, which require the user to provide annotations, these anal-
yses run on the original, unannotated source without human intervention. Full
automation and efficiency imply that such analyses are approximated. We impose
soundness: unlike testing, program behaviors are over-approximated so that, if
an error is present, it will be detected. However, the analysis can report spurious
errors. Sound static analyzers have been used for decades in applications, such as
program optimization, where precision is not critical. Recent progress had lead
to the design of tools able to check for simple but important safety properties
(such as the absence of run-time error) with few or zero false alarms. We partic-
ipated in the design of two of them [2]: Astrée, an industrial-strength analyzer
that checks for run-time errors in synchronous embedded C code, and AstréeA,
its prototype extension targeting multithreaded embedded C applications.
These analyzers are based on abstract interpretation, which is a general the-
ory of program semantics introduced by Cousot and Cousot in the late ’70s [5].
Abstract interpretation stems from the observation that many semantics can be
uniformly expressed as fixpoints of operators, after which seemingly unrelated
semantics can be compared. It expresses formally the fact that some seman-
tics are more abstract than (lose information with respect to) others as they
compute approximations in less rich domains of properties. It provides tools to
prove soundness: any property proved in an abstract semantic is still true in the
concrete one. Finally, it provides tools to design and combine abstractions.

The aim of the article is to give a short overview of the theory underlying
tools such as Astrée and AstréeA. Section 2 focuses on sequential programs and
presents the classic construction of an effective analyzer by abstraction of the
most concrete semantics of a program: its execution traces. Section 3 considers
multithreaded programs and explains how, borrowing ideas from rely/guarantee
proof methods [14], an efficient, thread-modular analysis can be constructed.
Section 4 briefly presents the Astrée and AstréeA analyzers, and Sec. 5 concludes.
The modest contribution of this article is the formulation, in Sec. 3.4, of
Jones’ rely/guarantee method [14] in abstract interpretation form, as an ab-
straction of the execution traces of multithreaded programs by decomposition
into intra-thread invariants and inter-thread interferences. This extends previ-
ous work in the 80’s [7] that formalized earlier Owicki–Gries–Lamport methods
[17,15] as abstract interpretation. It also extends our recent previous work [16],
that only considered coarse abstract interferences, by exhibiting an intermediate
layer of abstraction from which it can be recovered.

2 Abstractions for Sequential Programs

As a short introduction to abstract interpretation concepts, we review the formal


construction of a static analyzer for sequential programs by abstraction of its
trace semantics — see also [2, § II] for an extended introduction.
(1) i := 2; at (1): i = 0 ∧ n = 0
(2) n := input int(); at (2): i = 2 ∧ n = 0
(3) while i < n do at (3): 2 ≤ i ≤ max(2, n)
(4) if input bool() then i := i + 1; at (4): 2 ≤ i ≤ n − 1 ∧ n ≥ 3
(5) done; at (5): 2 ≤ i ≤ n ∧ n ≥ 3
(6) assert i >= 2; at (6): i = max(2, n)
(a) (b)

Fig. 1. A simple sequential program (a), and invariant assertions (b).

2.1 Transition Systems


The semantics of a program is defined classically [5] in a very general way in
small step operational form, as a transition system: (Σ, τ, I), where Σ is a set of
states, I ⊆ Σ is the subset of initial states, and τ ⊆ Σ ×Σ is a transition relation.
def
For sequential programs, the state set is defined as Σ = L × M, where each
state σ = (`, m) has a control part ` ∈ L denoting the current program point,
def
and a memory part m ∈ M = V → V mapping program variables V ∈ V to
values v ∈ V. A transition models an atomic execution step, such as executing a
machine-code instruction in a program. We denote by (`, m) → (`0 , m0 ) the fact
that ((`, m), (`0 , m0 )) ∈ τ , i.e, the program can reach the state (`0 , m0 ) from the
state (`, m) after one execution step. The transition system derives mechanically
from the program code itself, e.g., from the sequence of binary instructions or,
more conveniently, by induction on the syntax tree of the source code.
Example 1. Consider the simple programming language syntax:
0 0 00 0 00 0 00 0
`
P ` ::= ` x := e` | ` if e then ` P ` | ` while e do ` P ` | ` P ; ` P 0`
where ` and e denote respectively syntactic program points and expressions. The
0
transition system τ [` P ` ] is derived by induction on the syntax of P as follows:
0 def m
τ [` x := e` ] = { (`, m) → (`0 , m[x 7→ v]) | m ∈ M, e v}
` `00 `0 def 00 m 00 0
τ [ if e then P ] = { (`, m) → (` , m) | m ∈ M, e true } ∪ τ [` P ` ] ∪
m
{ (`, m) → (`0 , m) | m ∈ M, e false }
00 0 def m 00
τ [` while e do ` P ` ] = { (`, m) → (`00 , m) | m ∈ M, e true } ∪ τ [` P ` ] ∪
m
{ (`, m) → (`0 , m) | m ∈ M, e false }
00 0 def 00 00 0
τ [` P ; ` P 0` ] = τ [` P ` ] ∪ τ [` P 0` ]
where m[x 7→ v] denotes the function mapping x to v and y 6= x to m(y), and
m
e v states that e can evaluate to the value v in the memory m. 

2.2 Trace Semantics


We are not interested in the program itself, but in the properties of its executions.
Formally, an execution trace is a finite sequence of states σ0 → σ1 → · · · → σn
n n n

i i i

... ...

(a) (b) (c)

Fig. 2. Semantics of the program in Fig. 1 at various levels of abstractions: (a) traces,
and (b) reachable states and (c) intervals at program point (3).

such that σ0 ∈ I and ∀i, (σi , σi+1 ) ∈ τ . The semantics we want to observe, which
is called a collecting semantics, is thus defined in our case as the set T ∈ P(Σ ∗ )
of traces spawned by the program. It can be expressed [6] as the least fixpoint
of a continuous operator in the complete lattice of sets of traces:
T = lfp F where
def (1)
F (X) = I ∪ { σ0 → · · · → σi+1 | σ0 → · · · → σi ∈ X ∧ σi → σi+1 }

i.e., T is the smallest set containing traces reduced to an initial state and closed
under the extension of traces by an additional execution step. Note that we
are observing partial execution traces; informally, we allow executions to be
interrupted prematurely; formally, T is closed by prefix (it also includes the finite
prefixes of non-terminating executions). This is sufficient if we are interested in
safety properties, i.e., properties of the form “no bad state is ever encountered,”
which is the case here — more general trace semantics, able to also reason about
liveness properties, are discussed for instance in [4]. A classic result
S [6] is that
T can be restated as the limit of an iteration sequence: T = n∈N F n (∅). It
becomes clear then that computing T is equivalent to testing the program on
all possible executions (albeit with an unusual exhaustive breadth-first strategy)
and that it is not adapted to the effective and efficient verification of programs:
when the program has unbounded or infinite executions, T is infinite.
Example 2. The simple program in Fig. 1.a increments i in a loop, until it
reaches a user-specified value n. Figure 2.a presents its trace semantics starting
in the state set I = { (1, [m 7→ 0, i 7→ 0]) }. The program has infinite executions
(e.g., if i is never incremented). 

2.3 State Semantics


We observe that, in order to prove safety properties, it is not necessary to com-
pute T exactly. It is sufficient to collect the set S ∈ P(Σ) of program states
encountered, abstracting away any information available in T on the ordering of
states. We use an abstraction function αstate : P(Σ ∗ ) → P(Σ):
def
S = αstate (T ) where αstate (X) = { σ | ∃σ0 → · · · → σn ∈ X, i ∈ [0; n], σ = σi }
(2)
An important result is that, as T , S can be computed directly as a fixpoint:

S = lfp G where G(X) = I ∪ { σ 0 | ∃σ ∈ X, σ → σ 0 }


def

or, equivalently, as an iteration sequence S = n∈N Gn (∅), which naturally ex-


S
presses that S is the set of states reachable from I after zero, one, or more transi-
tions. The similarity in fixpoint characterisation of S and T is not a coincidence,
but a general result of abstract interpretation (although, in most cases, the ab-
stract fixpoint only over-approximates the concrete one: lfp G ⊇ αstate (lfp F ),
see [4]). Dually, given a set of states S, one can construct the set of traces
def
abstracted by S using a concretization function γ state = λS . { σ0 → · · · →
σn | n ∈ N ∧ ∀i ∈ [0; n], σi ∈ S }. The pair (αstate , γ state ) forms a Galois connec-
tion.1 A classic result [5] states that the best abstraction of F can be defined as
αstate ◦ F ◦ γ state , which in our case turns out to be exactly G. When the set of
states is finite (e.g., when the memory is bounded), S can always be computed
by iteration in finite time, even if T cannot. Obviously, S may be extremely
large and require many iterations to stabilize, hence computing S exactly is not
a practical solution; we will need further abstractions.

2.4 Program Proofs and Inductive Invariants

There is a deep connection [5] between the state semantics and the program logic
of Floyd–Hoare [13] used to prove partial correctness. If we interpret each logic
assertion A` at program point ` as the set of memory states J A` K ⊆ M satisfying
def
it, and note M = { (`, m) | m ∈ J A` K }, then (A` )`∈L is a valid (i.e., inductive)
invariant assertion if and only if G(M ) ⊆ M . Moreover, the best inductive in-
variant assertion stems from lfp G, i.e., it is S. While, in proof methods, the
inductive invariants must be devised by an oracle (the user), abstract interpre-
tation opens the way to automatic invariant inference by providing a constructive
view on invariants (through iteration) and allowing further abstractions.

Example 3. The optimal invariant assertions of the program in Fig. 1.a appear
in Fig. 1.b, and Fig. 2.b presents graphically its state abstraction at point (3). 

2.5 Memory Abstractions

In order gain in efficiency on bounded state spaces and handle unbounded ones,
we need to abstract further. As many errors (such as overflows and divisions by
zero) are caused by invalid arguments of operators, a natural idea is to observe
1
I.e., ∀X ∈ P(Σ ∗ ), ∀Y ∈ P(Σ), αstate (X) ⊆ Y ⇐⇒ X ⊆ γ state (Y ).
only the set of values each variable can take at each program point. Instead of
def
concrete state sets in P(Σ) = P(L×(V → V)), we manipulate abstract states in
def
ΣC = (L × V) → P(V). The concrete and abstract domains are linked through
the following Galois connection (so-called Cartesian Galois connection):
def
αcart (X) = λ(`, V ) . { v | ∃(`, m) ∈ X, m(V ) = v }
def (3)
γ cart (X ] ) = { (`, m) | ∀V, m(V ) ∈ X ] (`, V ) }

Assuming that variables are integers or reals, a further abstraction consists in


maintaining, for each variable, its bounds instead of all its possible values. We
compose the connection (αcart , γ cart ) with (the element-wise lifting of) the fol-
lowing connection (αitv , γ itv ) between P(R) and (R ∪ { − ∞ }) × (R ∪ { + ∞ }):
def
αitv (X) = [min X; max X]
def (4)
γ itv ([a; b]) = { x ∈ R | a ≤ x ≤ b }
yielding the interval abstract domain [5].
Another example of memory domain is the linear inequality domain [10]
that abstracts sets of points in P(V → R) into convex, closed polyhedra. One
abstracts P(Σ) ' L → P(V → V) by associating a polyhedron to each program
point, which permits the inference of linear relations between variables. The
polyhedra domain is thus a relational domain, unlike the domains deriving from
the Cartesian abstraction (such as intervals).

Example 4. Figure 2.c presents the interval abstraction of the program in Fig. 1.a
at point (3). The relational information that i ≤ n when n ≥ 2 is lost. 

Following the same method that derived the state semantics from the trace
one, we can derive an interval semantics from the state one: it is expressed as a
fixpoint lfp G] of an interval abstraction G] of G. While it is possible to define
an optimal G] as α ◦ G ◦ γ — where (α, γ) combines (αitv , γ itv ) and (αcart , γ cart )
— this is a complex process when G is large, and it must be performed for
each G, i.e., for each program. To construct a general analyzer, we use the
fact that F , and so, G = αstate ◦ F ◦ γ state , are built by combining operators
for atomic language constructs, as in Ex. 1. It is thus possible to derive G]
as a combination of a small fixed set of abstract operators. More precisely, we
only have to design abstraction versions of assignments, tests, and set union
∪. Generally, the combination of optimal operators is not optimal, and so, the
resulting G] is not optimal — e.g., (α ◦ G1 ◦ G2 ◦ γ)(X) ( ((α ◦ G1 ◦ γ) ◦ (α ◦
G2 ◦ γ))(X). Additionally, due to efficiency and practicality concerns, the base
abstract operators may be chosen non-optimal to begin with. Achieving a sound
analysis, i.e., γ(lfp G] ) ⊇ lfp G, only requires sound abstracts operators, i.e.,
∀X ] , (γ ◦ G] )(X ] ) ⊇ (G ◦ γ)(X ] ). The fact that G] 6= α ◦ G ◦ γ, and even the
existence of an α function, while sometimes desirable, is by no mean required —
for instance, the polyhedra domain [10] does not feature an abstraction function
α as some sets, such as discs, have no best polyhedral abstraction.
0 ]
Example 5. The interval abstraction J ` x := y + z ` K of a simple addition maps
the interval environment [x 7→ [lx ; hx ]; y 7→ [ly ; hy ]; z 7→ [lz ; hz ]] at point ` to
[x 7→ [ly + lz ; hy + hz ]; y 7→ [ly ; hy ]; z 7→ [lz ; hz ]] at point `0 , which is optimal. 
0 00 ]
Example 6. The optimal abstraction J ` y := −z; ` x := y + z ` K would map x
0 ] 0 00 ]
to [0; 0]. However, the combination J ` y := −z ` K ◦ J ` x := y + z ` K maps x to
[lz − hz ; hz − lz ] instead, which is coarser when lz < hz . 

Example 7. We can design a sound non-optimal fall-back abstraction for arbi-


0
trary assignments ` x := e` by simply mapping x to [−∞; +∞]. 

We now know how to define systematically a sound abstract operator G]


which can be efficiently computed. Nevertheless, the computation of lfp G] by
naive iteration may not converge fast enough (or at all). Hence, abstract domains
are generally equipped with a convergence acceleration binary operator O, called
widening and introduced in [5]. Widenings extrapolate between two successive
]
= Xi] O G] (Xi] ) converges in finite
def
iterates and ensure that any sequence Xi+1
time, possibly towards a coarse abstraction of the actual fixpoint.
Example 8. The classic interval widening [5] sets unstable bounds to infinity:
  
def −∞ if l2 < l1 +∞ if h2 > h1
[l1 ; h1 ] O [l2 ; h2 ] = ; (5)
l1 otherwise h1 otherwise

When analyzing Fig. 1, the iterations with widening at (3) give the following
intervals for i: i]0 = [2; 2] and i]1 = [2; 2] O [2; 3] = [2; +∞], which is stable. 

To balance the accumulation of imprecision caused by widenings and combining


separately abstracted operators, it is often necessary to reason on an abstract
domain strictly more expressive than the properties we want to prove — e.g., the
polyhedra domain may be necessary to infer variable bounds that the interval
domain cannot infer, although it can represent them, such as x = 0 in Ex. 6.

2.6 Further Considerations


We end this introduction with some pointers to additional material. Firstly, there
exists a large library of abstract domains, in particular numeric ones, and as-
sociated operators with various precision versus cost trade-offs — [2] describes
a few of them. An actual analyzer will use a combination, such as a reduced
product, of many domains [9]. Secondly, we have assumed for simplicity that the
set of variables V is finite, but abstract domains able to handle an unbounded
memory exist; this is necessary when dynamic memory allocation is used. They
often combine abstractions of the memory shape and of the contents of numeric
fields, as in [19]. Likewise, an efficient handling of procedures requires abstract-
ing the control state L (which can be large, or even unbounded in the case of
recursivity). Call-string methods [18] are an instance of such abstractions. Fi-
nally, alternate iteration techniques exist [3], as well as methods to decompose
the global fixpoint into several local ones to achieve a modular analysis [8].
3 Abstractions for Parallel Programs
We now consider multithreaded programs in a shared memory. We show how,
starting from a classic concrete semantics based on interleaved executions [11]
and applying abstract interpretation, we can construct an effective thread-mo-
dular static analysis which is similar to rely/guarantee proof methods [14].

3.1 Labelled Transition Systems


We assume a finite set T of threads. Each thread t ∈ T has its own control space
def
Lt and transition relation τt ⊆ Σt ×Σt , where Σt = Lt ×M. The state space and
transitions for the whole program are derived from those of individual threads as
def Q
follows. Program states live in Σ = ( t∈T { t } → Lt )×M, i.e., each thread has
its own control point, but the memory is shared. The semantics of the program
is defined as a labelled transition system (Σ, τ, I), where the transition relation
τ ⊆ Σ × T × Σ is defined as:2 ((`, ¯ m), t, (`¯0 , m0 )) ∈ τ if ((`[t],
¯ m), (`¯0 [t], m)) ∈
0 ¯ 0 ¯0 0
τt and ∀t 6= t, `[t ] = ` [t ]. It states that an execution step of the program is an
execution step of some thread and updates only that thread’s control location.
¯ m) → t
We denote such a step as (`, (`¯0 , m0 ). Labels t ∈ T are used to explicitly
remember which thread caused each transition.

3.2 Interleaved Trace Semantics


As for sequential programs, we consider finite prefixes of executions and ignore
liveness properties — in the context of parallel programs, liveness properties are
related to fairness conditions, which is a very complex issue not discussed here;
see [11] for a complete treatment. The trace semantics T is defined as in (1):

T = lfp F where
def 0 t i t 0 t ti−1 i t (6)
F (X) = I ∪ { σ0 → ··· → σi+1 | σ0 → · · · → σi ∈ X ∧ σi → σi+1 }

which thus corresponds to executions consisting of arbitrary interleavings of


transitions from any threads.
Given the similarity with the sequential trace semantics, a natural idea to
analyze a parallel program is to forget about labels and apply the state and
memory abstractions (Secs. 2.3, 2.5). The
Q problem with this method is the huge
number of controlSstates to consider: t∈T |Lt | instead of |L|. Moreover, τ is
much larger that t∈T τt as a single
Q transition in τt is repeated for each possi-
ble control state of other threads t0 6=t Lt0 . As a consequence, constructing an
abstraction G] of F from abstractions of atomic instructions requires a combi-
nation of many more such functions than for sequential programs. This makes
even coarse Cartesian interval abstractions impracticable.
2
The x̄ notations indicates that x̄ is a finite vector. x̄[i] is its value at index i. We
also use vector functions f¯. The i-th component of y’s image f¯(y) is denoted f¯(y)[i].
Finally, ¯ ¯ denote respectively ∅, ∪, ⊆ extended element-wise to vectors.
¯ , and ⊆
∅, ∪
(1) while true do (5) while true do at (1),(2),(4): x≤y
(2) if x < y then (6) if y < 10 then at (3): x<y
(3) x := x + 1; (7) y := y + 1; at (5),(6),(8): y ≤ 10
(4) done; (8) done; at (7): y < 10

(a) (b)

Fig. 3. A simple multithreaded program (a), and its invariant assertions (b).

(1) while true do (5) while true do at (1),(2),(4): x ∈ { − 2, 0 }


(2) x := x + 1; (6) x := x - 2; at (3): x ∈ { − 1, 1 }
(3) x := x - 1; (7) x := x + 2; at (5),(6),(8): x ∈ { 0, 1 }
(4) done; (8) done; at (7): x ∈ { − 2, −1 }

(a) (b)

Fig. 4. A program requiring flow-sensitivity (a), and its invariant assertions (b).

3.3 Rely/Guarantee
Proof methods for parallel programs, such as Owicki–Gries–Lamport [17,15] and
rely/guarantee
S [14] solve this issue by attaching invariantsQto thread control
points in t∈T Lt instead of combinations of thread points in t∈T Lt . The price
to pay is a refined notion of invariance: the properties attached to a thread t must
also be proved stable by the effect of the other threads t0 6= t. In rely/guarantee
methods, this effect is explicitly provided as extra annotations in t that are
assumed to hold (relied on) when checking the invariants for t and proved correct
(guaranteed) when checking the other threads t0 6= t. It then becomes possible
to check each thread in a modular way, i.e., without looking at the code of the
other threads, relying on the annotations instead.
Example 9. Figure 3 presents a program with two threads: the first one incre-
ments x up to y and the second one increments y up to 10. For conciseness, we
exemplify the rely/guarantee conditions at a single program point. Consider the
problem of proving that x < y holds at (3), just after the test. We need to prove
that the assertion is stable by the action of the second thread. It is sufficient to
rely on the fact that the second thread does not modify x and only increments
y. This property is, in turn, guaranteed by analyzing the second thread, relying
only on the fact that the first thread does not modify y. 

3.4 Interference Semantics


We now rephrase the idea of rely/guarantee methods as an abstract interpreta-
tion of the interleaved trace semantics T (6). We decompose the trace semantics
using two complementary abstractions: an abstraction into thread-local invari-
ants (which is inspired from the formalization in [7] of Owicki–Gries–Lamport
methods [17,15] as an abstract interpretation) and an abstraction into inter-
thread interferences (which is new). Firstly, the local invariant S̄[t] of a thread t
is defined by projecting, with the bijection πt , the reachable states αstate (T ) (2)
on t’s control state Lt :
def
S̄[t] = (Πt ◦ αstate )(T ) where
(7)
def
¯ m) def
Πt (X) = { πt (x) | x ∈ X } and πt (`, ¯ m[∀t0 6= t, pt0 7→ `[t
= (`[t], ¯ 0 ]])

¯ 0 ] of other threads t0 6= t encoded as extra variables


We keep the control state `[t
pt in the memory (called auxiliary variables in Owicki–Gries [17]). It is possible
0

to remove these variables and keep only t’s control information to get:
def
¯ πt (`,
αtctrl (S̄[t]) where αtctrl (X) = { (`, m) | ∃`, ¯ m) ∈ X ∧ `[t]
¯ = `} (8)

but αtctrl is known to be an incomplete abstraction: some invariance properties


on S̄[t] cannot be proved using αtctrl (S̄[t]) only (see Ex. 12). Secondly, the in-
terference Ā[t] of a thread t is defined as the possible actions it has on other
threads, as observed in the interleaved trace semantics T :

def 0t tn−1
Ā[t] = { (σi , σi+1 ) | ∃σ0 → · · · → σn ∈ T, ti = t } (9)

Interferences are actually subsets of the transition relation restricted to those


transitions appearing in actual traces starting in I. We can then express S̄[t] in
fixpoint form as a function of Ā and the transitions caused by thread t:

S̄[t] = lfp Gt (Ā) where


def t
Gt (Ȳ )(X) = Πt (I) ∪ { πt (σ 0 ) | ∃πt (σ) ∈ X, σ → σ 0 ∨ ∃t0 6= t, (σ, σ 0 ) ∈ Ȳ [t0 ] }
(10)
i.e., to reach a new program state from a known one, we either execute a step
t
from thread t (in →) or a step from another thread (in Ā[t0 ]). Moreover, we can
express directly Ā as a function of S̄:
t
Ā[t] = B̄(S̄)[t] where B̄(Ȳ )[t] = { (σ, σ 0 ) | πt (σ) ∈ Ȳ [t] ∧ σ → σ 0 }
def
(11)

which expresses that Ā[t] corresponds to the transitions in τt starting from a


reachable state. This yields the following nested fixpoint characterisation of S̄:
def
S̄ = lfp H̄ where H̄(X̄)[t] = lfp Gt (B̄(X̄)) (12)

can be computed in iterative form as: S̄ = ¯ n∈N H̄ n (∅)¯ while H̄(X̄)[t] =


S
which
n
S
n∈N (G t (B̄(X̄))) (∅). The benefit of this characterization is that the compu-
tation of H̄(X̄)[t] only depends on its argument X̄ and the transition relation
of the thread t, not on the transition relations of the other threads: the inner
fixpoint is thus similar to the analysis of a sequential process. However, as ana-
lyzing a thread in a given set of interferences B̄(X̄) gathers a new, larger set of
interferences B̄(H̄(X̄)), the analysis must be performed again with this enriched
set until it becomes stable. This is the role of the outer fixpoint.
3.5 Memory and Interference Abstractions

Our interference semantic S̄ is very concrete (the state semantics αstate (T ) can
be recovered fully from it), and too large to be computed directly. An effective
and efficient analyzer can be designed by applying abstractions independently
to local invariants and to interferences, while keeping the nested fixpoint form
of (12). Firstly, any memory abstraction, such as intervals (4) or polyhedra [10],
can be applied directly to each local invariant S̄[t]. As the number of variables
is often a critical parameter in the efficiency of abstract domains, a faster but
coarser analysis can be achieved by removing the auxiliary variables from S̄[t]
with αtctrl (8) prior to memory abstraction. Secondly, we abstract interferences
Ā[t]. For instance, it is possible to forget the control state of all threads using a
flow-insensitive abstraction αflow :

¯ `¯0 , ((`,
αflow (X) = { (m, m0 ) | ∃`,
def
¯ m), (`¯0 , m0 )) ∈ X }

Intuitively, this means that, when analyzing a thread t, we consider that any
instruction from any thread t0 6= t may be executed at any point of thread t.
Then, by noting that P((V → V)×(V → V)) ' P(({ 1, 2 }×V) → V), we can see
a set of pairs of maps on V as a set of maps on { 1, 2 } × V, with twice as many
variables; hence, we can apply any memory abstraction to αflow (X). Another
solution consists in abstracting the relation αflow (X) with its image, using the
following abstraction αimg , before applying a memory abstraction:

αimg (X) = { m0 | ∃m, (m, m0 ) ∈ X }


def

which is coarser but more efficient as it avoids doubling the number of variables.
By combining αflow , αimg , and αcart (3), we abstract thread interferences in
a non-relational and flow-insensitive way, as the set of values each thread can
store into each variable during the thread execution. Further abstractions, such
as intervals αitv (4), can be applied. This yields an efficient analysis: the abstract
interference is simply a global map from T × V to intervals, and we can build an
abstraction H̄ ] (X̄ ] )[t] of H̄(X̄)[t] by combining abstractions for each instruction
of thread t (as Ex. 5 but slight modified to take interferences into account).
0 ]
Example 10. The interval abstraction J ` x := y + 1` K in the environment [x 7→
[lx ; hx ]; y 7→ [ly ; hy ]] at ` and the global interference map [x 7→ [lx0 ; h0x ]; y 7→
[ly0 ; h0y ]] gives, at `0 , the interval x 7→ [min(lx0 , lx + min(ly , ly0 ) + 1); max(h0x , hx +
max(hy , h0y ) + 1)]. 

Example 11. We analyze Fig. 3 using intervals, by applying αtctrl , αcart , and αitv
to local invariants, and αflow , αimg , αcart , and αitv to interferences. We find that,
at (3), x ∈ [0; 9]. The information that x < y is lost. The second thread produces
the abstract interferences [x 7→ ∅, y 7→ [0; 10]], i.e., we infer that it cannot modify
x and can only put values from [0; 10] into y. We lose the information that y can
only increase. 
Example 12. Consider the program in Fig. 4. When analyzing the first thread,
we note that, at (2), x can be 0 or -2, depending on whether the second thread
is at (6) or (7). Moreover, to prove that x stays in { − 2, 0 } at (2), it is necessary
to infer that the interference from the second thread can only increment x when
it is at (7), and decrement it when it is at (6). A flow-insensitive abstraction
using αtctrl and αflow would instead infer that x can be incremented arbitrarily
and decremented arbitrarily. It would not allow us to prove that x is bounded. 

3.6 Further Considerations


A practical consequence of (12) is that a thread-modular static analyzer can be
designed by slightly modifying a sequential analyzer: all we need to do is keep
track of (abstract) interferences as well as reachable states, and add an external
fixpoint iteration to re-analyze each thread until the inferred interferences sta-
bilize. As for memory abstractions, there are many choices in how to abstract
interferences, with various cost versus precision trade-offs and various levels of
expressiveness. Although we presented only flow-insensitive non-relational ab-
stract interferences, we can envision abstractions keeping (at least partially)
flow and relational information. This is in contrast to our earlier formalization
[16] which only allowed flow-insensitive non-relational interferences, and can now
be seen a special case of the framework presented here. In the presence of mu-
tual exclusion primitives and scheduling policies that restrict the interleaving of
threads (such as thread priorities), a model of the scheduler can be incorporated
into the semantics to refine the notion of interference. For instance [16] handles
locks by partitioning flow-insensitive, non-relational abstract interferences with
respect to the set of locks each thread holds. Another remark is that the inter-
leaved trace semantics (6) is not realistic in the context of modern multicore
architectures and compilers; non-coherent caches and optimizations can expose
extra behaviors. However, we proved in [16] that, provided that a flow-insensitive
abstraction of interferences is used, the abstract semantics is sound even in the
presence of weakly consistent memory models, so that the results of the static
analysis can still be trusted in realistic settings. Finally, we did not discuss the
case of an unbounded number of threads, which requires further abstraction.

4 Applications
We now briefly describe two static analysis tools that were designed at the École
normale supérieure using abstract interpretation principles.

4.1 Astrée
Astrée is a static analyzer that checks for run-time errors in synchronous (hence
sequential) embedded critical C programs. It covers the full C language, in-
cluding pointers and floats, but excluding dynamic memory allocation and re-
cursivity (forbidden in the realm of critical software). It checks for arithmetic
overflows, and invalid arithmetic and memory operations. It is sound, and so,
never misses any existing error. Due to over-approximations, it can signal false
alarms. Although Astrée can analyze many kinds of codes, it is specialized to
control-command avionic software, on which it aims at efficiency and certifica-
tion, i.e., zero false alarm. It was able to prove quickly (2h to 53h) the absence of
run-time error in large (100 K to 1M lines) industrial avionic codes. This result
could be achieved using a specialization process: starting from a fast and coarse
analyzer based on intervals, we added manually more powerful abstract domains
as they were needed. Some were borrowed from the large library of existing
abstractions, and others were developed specifically for the avionic application
domain (such as an abstraction of digital filters [12]). Astrée is a mature tool:
it is industrialized and made commercially available by AbsInt [1]. More details
can be found in [2].

4.2 AstréeA

AstréeA aims at checking for run-time errors in multithreaded embedded critical


C programs. It is based on the Astrée code-base and inherits all of its abstrac-
tions. Additionally, it implements the interference fixpoint analysis described in
Sec. 3. Interferences are abstracted in a flow-insensitive and non-relational way,
which was sufficient to give encouraging experimental results. On our target
application, a 1.6 Mlines industrial avionic software with 15 threads, AstréeA
reports around 1 300 alarms in 50h. More details can be found in [16,2]. AstréeA
is a research prototype in development; we are currently improving it with more
powerful memory and interference abstractions.

5 Conclusion

We have provided in this article a short glimpse of abstract interpretation theory


and its application to the systematic construction of static analyzers for sequen-
tial and parallel programs. Two key reasons make abstract interpretation-based
constructions attractive. Firstly, its ability to relate in a formal way the output
of a static analyzer all the way down to the precise dynamic behavior of the
input program (its execution traces), both in terms of soundness proof and in-
formation loss. Secondly, the ability to decompose the design of an analyzer as
the combination of independent, reusable abstractions, which allows the modular
implementation of analyzers (e.g., abstract domains designed for Astrée could
be reused in AstréeA). Future work includes the design of new abstractions
to improve the AstréeA analyzer prototype and reduce the number of alarms
on selected embedded multithreaded C programs. In particular, the derivation
of thread-modular abstract semantics for parallel programs through the use of
concrete interferences we introduced here opens the way to the design of flow-
sensitive and relational interference abstractions, which was not possible in the
earlier framework underlying AstréeA.
Acknowledgments. We thank the anonymous referees on a earlier version of
[16] for pointing out the link between the analysis performed by AstréeA and
rely/guarantee methods, which was not apparent to us before. We also thank
Patrick Cousot for urging us to provide a sequence of explicit abstraction func-
tions detailing precisely which information is lost at each step of our construction,
instead of providing a monolithic soundness proof as we did in [16].

References
1. AbsInt, Angewandte Informatik. Astrée run-time error analyzer. http://www.
absint.com/astree.
2. J. Bertrane, P. Cousot, R. Cousot, J. Feret, L. Mauborgne, A. Miné, and X. Rival.
Static analysis and verification of aerospace software by abstract interpretation. In
AIAA Infotech@Aerospace, number 2010-3385 in AIAA, pages 1–38. AIAA (Amer-
ican Institute of Aeronautics and Astronautics), Apr. 2010.
3. F. Bourdoncle. Efficient chaotic iteration strategies with widenings. In Proc. of the
Int. Conf. on Formal Methods in Programming and their Applications (FMPA’93),
volume 735 of LNCS, pages 128–141. Springer, June 1993.
4. P. Cousot. Constructive design of a hierarchy of semantics of a transition system
by abstract interpretation. Theoretical Computer Science, 277(1–2):47–103, 2002.
5. P. Cousot and R. Cousot. Abstract interpretation: A unified lattice model for
static analysis of programs by construction or approximation of fixpoints. In Proc.
of the 4th ACM Symp. on Principles of Programming Languages (POPL’77), pages
238–252. ACM, Jan. 1977.
6. P. Cousot and R. Cousot. Constructive versions of Tarski’s fixed point theorems.
Pacific Journal of Mathematics, 81(1):43–57, 1979.
7. P. Cousot and R. Cousot. Invariance proof methods and analysis techniques for
parallel programs. In Automatic Program Construction Techniques, chapter 12,
pages 243–271. Macmillan, New York, NY, USA, 1984.
8. P. Cousot and R. Cousot. Compositional separate modular static analysis of pro-
grams by abstract interpretation. In Procs. of the 2d Int. Conf. on Advances in
Infrastructure for E-Business, E-Science and E-Education on the Internet (SS-
GRR’01). Scuola Superiore G. Reiss Romoli, Aug. 2001.
9. P. Cousot, R. Cousot, J. Feret, L. Mauborgne, A. Miné, D. Monniaux, and X.
Rival. Combination of abstractions in the Astrée static analyzer. In Proc. of the
11th Annual Asian Computing Science Conf. (ASIAN’06), volume 4435 of LNCS,
pages 272–300. Springer, Dec. 2006.
10. P. Cousot and N. Halbwachs. Automatic discovery of linear restraints among
variables of a program. In Conf. Rec. of the 5th ACM SIGPLAN/SIGACT Symp.
on Principles of Programming Languages (POPL’78), pages 84–97. ACM, 1978.
11. R. Cousot. Fondements des méthodes de preuve d’invariance et de fatalité de pro-
grammes parallèles. Thèse d’État ès sciences mathématiques, Institut National
Polytechnique de Lorraine, Nancy, France, 15 Nov. 1985.
12. J. Feret. Static analysis of digital filters. In Proc. of the 13th Europ. Symp. on
Programming (ESOP’04), volume 2986 of LNCS, pages 33–48. Springer, Mar. 2004.
13. C. A. R. Hoare. An axiomatic basis for computer programming. Commun. ACM,
12(10):576–580, Oct. 1969.
14. C. B. Jones. Development Methods for Computer Programs including a Notion of
Interference. Phd thesis, Oxford University, Jun. 1981.
15. L. Lamport. Proving the correctness of multiprocess programs. IEEE Trans. on
Software Engineering, 3(2):125–143, Mar. 1977.
16. A. Miné. Static analysis of run-time errors in embedded real-time parallel C pro-
grams. Logical Methods in Computer Science (LMCS), 8(26):1–63, Mar. 2012.
17. S. Owicki and D. Gries. An axiomatic proof technique for parallel programs I.
Acta Informatica, 6(4):319–340, Dec. 1976.
18. M. Sharir and A. Pnueli. Two approaches to interprocedural data flow analysis.
In Program Flow Analysis: Theory and Applications, pages 189–233. Prentice-Hall,
1981.
19. A. Venet. Nonuniform alias analysis of recursive data structures and arrays. In
Proc. of the 9th Int. Symp. on Static Analysis (SAS’02), volume 2477 of LNCS,
pages 36–51, 2002.

A Proofs
All the results from Sect. 2 are very classic, so, we do not prove them here (see
for instance [4]). Instead, we focus on the new results presented in Sect. 3: the
nested fixpoint characterization of the reachable states of a parallel program
using interferences.

A.1 Proof of (6)


The proof that the operator F in (6) indeed has a least fixpoint, T , is analogous
to that of F in (1). As F is a continuous morphismS in the
S complete partial order
of trace sets ordered by inclusion, i.e., F ( i Xi ) = i F (Xi ), we can apply
Kleene’s fixpoint theorem [4], which states that lfp F exists and is exactly equal
to n∈N F n (∅).
S
We now repeat this classic proof for the sake of completeness. Let us note
def S n
X = n∈N F (∅). We first prove that X is indeed a fixpoint S of F , then
n
that it is smaller than any other fixpoint. Indeed, F (X) = F ( n∈N F (∅)) =
n+1 0 1
S
n∈N F (∅), by continuity. Moreover, F (∅) = ∅ ⊆ F (∅), hence F (X) =
n+1
(∅) ∪ F 0 (∅) = n∈N F n (∅) = X. Hence, X is a fixpoint. Let Y be an-
S S
n∈N F
other fixpoint, i.e., F (Y ) = Y . We have F 0 (∅) = ∅ ⊆ Y and, if F n (∅) ⊆ Y , then
F n+1 (∅) = F (F n (∅)) ⊆ F (Y ), as the continuity of F implies its monotonicity,
hence F n+1 (∅) ⊆ Y as Y = F (Y ). Thus, by recurrence, ∀n, F n (∅) ⊆ Y , which
implies that X ⊆ Y . Hence, X is the least fixpoint of F .

A.2 Proof of (10)


def def
By definition (7), S̄[t] = Πt (αstate (T )) and T = lfp F with F (X) = I ∪
t0 t i t 0ti−1 t i
{ σ0 → ··· → σi+1 | σ0 → · · · → σi ∈ X ∧ σi → σi+1 }. We wish to prove
def t
that S̄[t] = lfp Gt (Ā), where Gt (Ā)(X) = Πt (I) ∪ { πt (σ 0 ) | ∃πt (σ) ∈ X, σ →
def
σ 0 ∨ ∃t0 6= t, (σ, σ 0 ) ∈ Ā[t0 ] }. To lighten notations, we note G = Gt (Ā).
state
We first prove by recurrence on n that Πt (α (F (∅))) = Gn (∅). Intu-
n
n
itively, G (∅) corresponds to the states, projected on thread t, that can be
reached from I after at most n execution steps. When n = 0, Πt (αstate (F 0 (∅))) =
G0 (∅) = ∅. Assume that the property holds for some n and let us prove that
it holds for n + 1. As πt is one-to-one, we can reason equivalently on Gn+1 (∅)
and the states σ such that πt (σ) ∈ Gn+1 (∅), so, let us consider such a σ. Then,
either (a) σ ∈ I, or σ can be reached from some σ 0 such that πt (σ 0 ) ∈ Gn (∅)
t
and either (b) σ 0 → σ or (c) ∃t0 6= t, (σ 0 , σ) ∈ Ā[t0 ]. By definition, (σ 0 , σ) ∈ Ā[t0 ]
t0
is equivalent to the existence of a trace · · · σ 0 → σ · · · in T . Moreover, by recur-
rence hypothesis, σ 0 ∈ αstate (F n (∅)), i.e., there exists a trace in T with at most
t0
n transitions and ending in σ 0 . This means that, if a transition · · · σ 0 → σ · · ·
exists in a trace in T , it also exists in a trace of length at most n + 1 in T (the set
of traces is “closed by fusion” [11]). Hence, case (c) is equivalent to the existence
t0
of t0 6= t and a trace · · · σ 0 → σ · · · in F n+1 (∅). Case (b) is equivalent to the
t
existence of a trace · · · σ 0 → σ · · · in F n+1 (∅). Thus, the disjunction of (a), (b),
and (c) is equivalent to the existence of a trace in F n+1 (∅) containing σ, which
is equivalent to σ ∈ αstate (F n+1 (∅)). This ends the proof by recurrence.
We now use the characterization of fixpoints by iteration: we proved in
the proofS of (6) that T = n∈N F n (∅). Kleene’s theorem implies likewise that
S
n
lfp G = S n∈N G (∅). By continuity of Πt and αstate , we get that Πt (αstate (T )) =
state n
( n∈N F (∅))) = n∈N Πt (αstate (F nS(∅))). Applying the result of the
S
Πt (α
preceding paragraph, we get Πt (αstate (T )) = n∈N Gn (∅) = lfp Gt (Ā).

A.3 Proof of (11)


t
We now prove that Ā[t] = { (σ, σ 0 ) | πt (σ) ∈ S̄[t] ∧ σ → σ 0 }. By definition,
def t
0 tn−1
Ā[t] = { (σi , σi+1 ) | ∃σ0 → · · · → σn ∈ T, ti = t }. As T is closed by prefix, we
0 t tn−1
have Ā[t] = { (σn−1 , σn ) | ∃σ0 → · · · → σn ∈ T, tn−1 = t }. As T is a fixpoint
0 t tn−1 0 t tn−2
of F , ∃σ0 → · · · → σn ∈ T is equivalent to ∃σ0 → · · · → σn−1 ∈ T and
tn−1
σn−1 → σn . Finally, as S̄[t] = Πt (αstate (T )), this is equivalent to πt (σn−1 ) ∈
tn−1
S̄[t] and σn−1 → σn , which ends the proof.

A.4 Proof of (12)


def
We now prove that S̄ = lfp H̄, where H̄(X̄)[t] = lfp Gt (B̄(X̄)), and give the
iterative form of lfp H̄.
Firstly, we prove that S̄ is a fixpoint of H̄. Indeed, for any t, H̄(S̄)[t] =
lfp Gt (B(S̄)) = lfp Gt (Ā) by (11), which equals S̄[t] by (10).
Secondly, we prove that lfp H̄ can be expressed in iterative form. To do
so, we prove that H̄ is continuous. Kleene’s fixpoint theorem applied to the
continuous function Gt gives the following characterization of H̄: H̄(X̄)[t] =
X̄)))n (∅). To simplify notations, we define, for each t and n, the
S
n∈N (G t (B̄(
function I¯n (X̄)[t] = (Gt (B̄(X̄)))n (∅). Then, we have H̄(X̄) = ¯ n∈N I¯n (X̄). We
def S

note that each I¯n is continuous. Hence, we have: H̄( ¯ i X̄i ) = ¯ n∈N I¯n ( ¯ i X̄i ) =
S S S
¯
S ¯ I¯n (X̄i ) = S
S ¯ S¯ ¯ ¯ H̄(X̄i ), which proves the continuity of H̄.
S
n∈N i i n∈N In (X̄i ) = i
We can thus apply Keene’s fixpoint theorem to H̄ itself to get lfp H̄ in iterative
form: lfp H̄ = ¯ n∈N H̄ n (¯
S
∅).
¯
Finally, we prove that S̄ ⊆lfp H̄. To do this, we prove by recurrence on n that,
t0 tn−1
if σn is reachable after n steps, i.e., if there exists some trace σ0 → · · · → σn in
T , then, for any t, πt (σn ) ∈ (H̄ n+1 (¯∅))[t]. When n = 0, H̄(¯∅)[t] = lfp Gt (B̄(∅)) =
lfp Gt (∅) ⊇ Πt (I). As σ0 ∈ I, we indeed have πt (σ0 ) ∈ Πt (I). Assume that the
t0 tn
property is true at rank n and consider a trace σ0 → ··· → σn+1 in T . As T is
t0 tn−1
closed by prefix, σ0 → · · · → σn is also in T and we can apply the recurrence
tn+1
hypothesis to get πt (σn ) ∈ (H̄ n+1 (¯∅))[t]. Moreover, we have σn → σn+1 . We
consider first the case tn+1 = t. Then as (H̄ n+1 (¯∅))[t] is closed by reachabil-
ity from thread t, we also have πt (σn+1 ) ∈ (H̄ n+1 (¯∅))[t], and so, πt (σn+1 ) ∈
(H̄ n+2 (¯
∅))[t]. Consider now the case tn+1 6= t. Then, πt (σn ) ∈ (H̄ n+1 (¯∅))[t] im-
plies that (σn , σn+1 ) ∈ B̄(H̄ n+1 (¯∅))[t]. As a consequence, (H̄ n+2 (¯∅))[t] is closed
tn+1
under reachability through the transition σn → σn+1 , and so, πt (σn+1 ) ∈
(H̄ n+2 (¯
∅))[t]. We have proved that, forS any πt (σ) ∈ S̄[t], there exists some n such
that πt (σ) ∈ (H̄ n (¯
∅))[t]. As lfp H̄ = ¯ n∈N H̄ n (¯∅), we get that πt (σ) ∈ (lfp H̄)[t],
hence, S̄ ⊆¯ lfp H̄.

You might also like