|
September 2008Introduction Recently Andy King and Harald Sondergaard proposed a new method for synthesizing invariants of integer programs with bit-level semantics. The method is based on abstract interpretation. The main idea is to abstract relations between input and output bit vectors as a set of congruence equations modulo 2w, where w is the length of the bit vectors. The computation of the abstract fixed point is based on congruence analysis and Boolean reasoning, implemented using SAT solvers. King and Sondergaard compute invariants as follows: a program is divided in basic blocks. Each basic block is associated with a so-called summary. A summary represents an invariant for the given block of the program. Summaries are computed using a SAT solver: given a command and a summary of the input block, the algorithm incrementally computes a summary of a output block. Using this technique King and Sondergaard were able to synthesize invariants that no other analysis was able to derive. In the following we show that one can use an SMT solver to get a more concise implementation of this abstract interpretation using directly bit vectors. We illustrate this idea on the same example that was used in the original paper - Wegner's fast bit counting algorithm. The algorithm takes a bit vector x as input a and returns the number c of 1 bits in x.
y = x;
c = 0;
while (y != 0) {
y &= y - 1;
c++;
}
We give a top-down description of our solution. First we describe how we represent programs in Z3. A program P is as a tuple <n1, n2, E, m>, where n1 denotes the number of program locations or basic blocks in P, n2 denotes the number of variables in P, and set E represents the set of control-flow edges. The integer m is a modulus for later representations of congruence equations. A control-flow edge is modeled as a tuple <l1, l2, c>, where l1 is the entry location, l2 the exit location and c a command. A command consists of a guard and a sequence of assignments of terms to program variables. Here is the complete specification of a program in Z3:
The following program graph represents Wegner's fast bit counting algorithm. It contains three locations and three variables (x, y and z).
The declaration of this program in Z3 looks as follows:
As mentioned previously, the abstract program associates with each program location a linear congruence. Naively speaking, the set of solutions of such a linear congruence describes the set of values that variables can have at the given program location in the concrete program. For describing the current state of the fixed point iteration of the abstract interpretation, we introduce the type abstract_interpretation_state. It contains the program and a sequence of congruences, one for each program location. It further contains a "todo" list which contains the list of locations that still need to be processed in order to reach the fixed point. The type todo_list also contains a Boolean array ("active") that keeps a flag for each location, indicating whether the location was changed during the fixed point computation. The precise definition of the type abstract_interpretation_state is as follows:
In order to explain how to construct the initial abstract interpretation state, first we define a set of linear congruences. It is modeled using a matrix A, a vector b and a modulus m: A x ≡m b. We represent matrix A as an array of rows and each row is an array of integers. The condition wf checks whether the linear congruence is well formed, i.e. whether coefficients are in Zm = {0, 1, ..., m-1} and whether the number of rows in A corresponds to the number of rows in b. The matrix A has n*k columns, where n is the length of a bit vector, and k is the number of program variables.
Next, we need to define the supremum and infimum of the abstract lattice. The supremum is given by the linear congruence 0 x ≡ 0. Every vector x is in the set of solutions of this congruence, i.e. it denotes the universal set "True" which is the supremum of the concrete lattice. The matrix A consists of one row and the dimensions of that row is determined by the number of program variables. The function call "zeroVec dim" returns the vector [| 0; 0; ... ;0 |] of a dimension dim. For example, "zeroVec 3" produces a vector [| 0; 0; 0 |]. The infimum of the abstract lattice denoting the empty set "False" is constructed similarly.
In the initial state of the abstract interpretation every single value is admitted at location 0 (the initial location) and all other locations are associated with the empty set. For this reason we associate the congruence 0 x ≡ 0 with location 0 and to all other locations we associate the congruence 0 x ≡ 1. Furthermore, we add location 0 into "todo" list.
Incremental Construction of Congruences We now briefly describe how the incremental construction of congruences works. For a given program location l, let Fl be a formula denoting a set of values that program variables can have in the location l. Let e=(l_pre,l_post,c) be a control-flow edge and let Te be a formula constructed from the command c associated with the edge e that represents the transition relation of command c. We will explain how to construct these formula later. Given the formula Fl_pre(x) and the formula Te(x, x') we iteratively construct a concruence Fl_post(x') that abstracts the successor states of Fl_pre(x) under command c. Initially we set Fl_post(x') to the current concruence associated with location l_post. We then iteratively check the satisfiability of H (x, x') ≡ Fl_pre(x) AND Te(x, x') AND NOT Fl_post(x') by calling Z3. If H (x, x') is unsatisfiable, then the formula Fl_post covers successor states of Fl_pre under command c and we are done. However, if the formula H(x, x') is satisfiable, then there are some values (v,v') that make it satisfiable. The values v' describe states at location l_post that are not yet covered by formula Fl_post. We need to refine the concruence assoicated with l_post so that it encompasses the values v'. For checking satisfiability of formula H (x, x') we use Z3 with the parameter for "MODEL" set to "true". It means that if the formula is satisfiable, Z3 will also return one of its models. We then extract the values v' from the model that are used for the refinement of the congruence associated with location l_post. This refinement is done using operations join and project. We incrementally continue with this procedure until we find the most precise abstraction. The above procedure of propagating constraints to all locations is implemented in the following loop. It takes the abstract interpretation state and first checks the emptiness of the todo list. If the todo list is empty, the algorithms ends and each location contains a congruence that describes an invariant for that location in the concrete program. However, if there still exists some location in the todo_list, we call function "propagate_location location state" which takes the abstract interpretation state and the location from the todo list and propagates the constraints for all its outgoing control-flow edges:
We explain now how the basic ideas described at the beginning of this section are encoded in the function propagate_location. For this we use the function propagate_edge. Let fmla be a Z3 formula that corresponds to H (x, x') (later in the text we explain all the technical details about constructing Z3 formulas from edges and linear congruences). Using Z3 we try to construct a model for fmla. If a model is not found, then the formula is unsatisfiable and we stop with further abstraction refinements. If a model is found, then we have to update the linear congruence that corresponds to the location l_post. Let lc2 be a linear congruence corresponding to location l_post. We update lc2 as follows: first, based on the returned model we construct a new linear congruence lc1. The set of solutions for the new linear congruence for l_post should be the union of the set of solutions for lc2 and of the set of solutions for lc1. This new linear congruence is derived by computing the join of lc1 and lc2. Note that the join does not simply correspond to the union of the set of solutions, because linear congruences are not closed under union. Basically, in order to compute the join we compute the set of generators for both congruences and then we take the union of those generators. We explain the details later. Once the new lc2 is computed, we update it in the abstract interpretation state. We add the start and end point of the edge in the todo list since we do not know whether we already reached a fixed point or not. The call "let ty = z3.MkBvType(uint32 (log2_ceiling state.congruences.[0].m))" creates a bit-vector type of size ⌈log 2m⌉ since we use the theory of bit-vectors to handle modular arithmetic.
We still need to explain how to handle primed variables. We use the following command to create a variable of the type ty in Z3: z3.MkConst(z3.MkSymbol(some integer identifier), ty)) For a program P we use integers 0,...,P.num_vars - 1 as identifiers for unprimed program variables. For primed variables we use integers P.num_vars,...,P.num_vars + P.num_vars - 1. Calling function get_primed_vars on a program P returns the list of Z3 variables that corresponds to the list of primed variables of program P.
Constructing a Z3 Formula for the Transition Relation of a Control-flow Edge Every control-flow edge in a program P contains a command c. This command consists of two parts: a guard and a sequence of variable assignments. Assuming that we are able to construct the Z3 formula for each part, the resulting edge formula would be a conjunction of both those formulas. Let mk_term be a function that takes a term t and returns the corresponding Z3 term. It can be easily defined using pattern matching. The guard that is associated with a command can be translated directely into a Z3 formula using function mk_term. The assigments are translated into equations between primed and unprimed variables.
Constructing a Z3 Formula from a Linear Congruence In this section we explain how to construct a Z3 formula that corresponds to the congruence A x ≡m b. The function mk_formulaFromCongruence_bitwise has two parameters: the linear congruence lc and the integer offset. The last parameter is used for handling primed and unprimed variables. Let (l1, l2, c) be a control-flow edge. If function mk_formulaFromCongruence_bitwise is invoked for constructing the formula corresponding to the congruence at location l1 then the offset has value zero and if it is invoked for location l2 then the resulting formula is supposed to range over primed variables, thus, the offset has value program.num_vars. Let mk_eq be a function that takes the ith row of the matrix A and returns the Z3 formula that corresponds to A.[i] x ≡m b.[i]. Note that this is a single linear congruence, not a set. Then, the Z3 formula that corresponds to the whole system A x ≡m b is a conjunction of all such row formulas. For a given row (r.[0]; ... ; r.[n-1]) and index idx the corresponding Z3 formula is given by the equality a = b.[idx], where a is the product of the sum of all r.[j] and the variable at position j. Recall that the variable vector has dimension n*k, where n is the length of a bit vector and k is the number of program variables. For our sample program the variable vector looks as follows: (x0, ..., x7, y0, ..., y7, c0,..., c7). Let x be a variable vector in the linear congruence system A x ≡m b. Then the Z3 term corresponding to xj is constructed as follows:
Constructing a Linear Congruence from a Model Given a model that is produced by Z3, we want to construct the linear congruence whose set of solutions corresponds to that model. The function extract_linear_congruence_bitwise takes as parameters the model, the list of variables in which we are interested, and the modulus m. Let model(x) be the numerical value that the model assigns to variable x. Then we construct the linear congruence from the model as follows: A is the identity matrix and vector b consists of model(x) values. The identity matrix of the dimension d is constructed by invoking function idMatrix d.
Vector b is constructed from the returned values as follows: let var be a variable that belongs to the list of input parameters of extract_linear_congruence_bitwise. The numerical value of var in the model can be determined with the following command: model.GetNumeralValueInt(model.Eval(var)) The corresponding value is then passed to the function expand_val which takes an integer as parameter and creates a list that corresponds to the binary representation of that integer. Finally, vector b is constructed by concatenating all those lists of binary numbers.
Constructing the Join of two Linear Congruences To construct the join of two linear congruences, we will use results presented in the paper by King and Sondergaard. They mostly rely on the results shown in the paper by Müller-Olm and Seidl on analysis of modular arithmetic. Each congruence system Ax =m b defines an affine subset of Zm. This affine subset can be described through the set of its generators. Calculating the join of two affine subset corresponds to calculating the set that is generated by the union of their generators. Let lc be a linear congruence. Then True join lc = True and False join lc = lc.
Computing the join for non-trivial congruences is more involved. For this purpose we use Proposition 3 in the paper by King and Sondergaard from which an algorithm for computing the join can be derived. This algorithm looks as follows:
We briefly explain each of those steps. The first step is a straight-forward encoding of the bigger matrix using the pattern shown above.
To triangulate the matrix obtained in the first step we use the algorithm described in the paper by Müller-Olm and Seidl. To obtain the triangulated matrix we cannot just apply standard Gaussian eliminations, since there are zero divisors in Zm. Therefore, we could not use the matrix library that exists in F#, because it does not support modular arithmetic. The algorithm of Müller-Olm and Seidl takes modular arithmetic into account. The implementation corresponds closely to their algorithm. At the end we invoke the function updateMatrix which removes potential zero vectors from the matrix.
The last step is to project the triangulated matrix onto the variable vector. Since the matrix is in triangulated form, we use Proposition 2 from the paper by King and Sondergaard. First we calculate the dimension for the projection which is the top-most row j of the matrix A for which (A(j,1), . . . , A(j,i-1)) = 0 holds.
Once we find the dimension for the projection, we just extract the submatrix from the triangulated matrix
Acknowledgements Nikolaj Bjørner, James Margetson, Daniel Balasubramanian, Utkarsh Upadhyay, Thomas Wies and Damien Zufferey who looked at previous versions of the code above and made helpful suggestions. |
|