Bit-blasting
Bit-blasting decides formulas over fixed-width bit-vectors, the machine integers of programs and hardware, by reducing them to propositional satisfiability. Each -bit variable becomes Boolean variables, each operation becomes a Boolean circuit over those bits, the circuits are encoded as clauses with the Tseitin transformation, and a SAT solver decides the result[1]. Addition becomes a ripple-carry adder, comparison a chain of bit comparisons, and multiplication an array of adders. The method is complete, since every bit-vector operation is a finite Boolean function, and it is how SMT solvers decide the theory of bit-vectors after simplifying at the word level[3][4][5]. Its weakness is multiplication, whose circuits are quadratic in the width and notoriously hard for SAT solvers to reason about[7].
Bit-vector semantics
A bit-vector of width is a sequence of bits, read as an unsigned integer in or as a two's-complement integer in . Arithmetic is modulo : is , and overflow wraps around, exactly as in machine arithmetic. The SMT-LIB theory of fixed-size bit-vectors, QF_BV in its quantifier-free form, provides arithmetic, bitwise operations, shifts, signed and unsigned comparisons, concatenation and extraction[11]. Bit-precise reasoning is what program analysis needs: a verifier that treats machine integers as mathematical integers misses overflow bugs, and one that treats them as bit-vectors finds them.
Encoding
Every Boolean gate becomes a fresh variable with clauses that force it to equal its function of the inputs[2]:
Arithmetic is built from these. A full adder computes the sum bit and the carry of three bits:
and of them in a chain add two -bit numbers modulo , using gates. Multiplication adds shifted partial products , using gates. Equality is a conjunction of bitwise equivalences, and unsigned less-than a chain that decides at the most significant differing bit[1].
A small DPLL solver with watched literals, used by the examples.
(* A small DPLL solver with two watched literals and no clause learning,enough for the circuits below. Literals are non-zero integers. *)type t = {mutable nvars : int;mutable clauses : int array list;}let create () = { nvars = 0; clauses = [] }let fresh s = s.nvars <- s.nvars + 1; s.nvarslet add s c = s.clauses <- Array.of_list c :: s.clauseslet decisions = ref 0let solve s =let n = s.nvars inlet cls = Array.of_list s.clauses inlet assign = Array.make (n + 1) 0 and trail = Array.make (n + 1) 0 and len = ref 0 and qhead = ref 0 inlet idx l = if l > 0 then 2 * l else (2 * -l) + 1 inlet watches = Array.make ((2 * n) + 2) [] inlet value l = let v = assign.(abs l) in if l > 0 then v else -v inlet set l = assign.(abs l) <- (if l > 0 then 1 else -1); trail.(!len) <- l; incr len inlet units = ref [] inArray.iteri (fun ci c ->if Array.length c = 1 then units := c.(0) :: !unitselse (watches.(idx c.(0)) <- ci :: watches.(idx c.(0)); watches.(idx c.(1)) <- ci :: watches.(idx c.(1)))) cls;let propagate () =let ok = ref true inwhile !ok && !qhead < !len dolet f = - trail.(!qhead) inincr qhead;let pending = ref watches.(idx f) inwatches.(idx f) <- [];while !pending <> [] dolet ci = List.hd !pending inpending := List.tl !pending;let c = cls.(ci) inif c.(0) = f then (c.(0) <- c.(1); c.(1) <- f);let keep () = watches.(idx f) <- ci :: watches.(idx f) inif value c.(0) = 1 then keep ()else beginlet k = ref 2 inwhile !k < Array.length c && value c.(!k) = -1 do incr k done;if !k < Array.length c then (c.(1) <- c.(!k); c.(!k) <- f; watches.(idx c.(1)) <- ci :: watches.(idx c.(1)))else beginkeep ();if value c.(0) = -1 then (ok := false; watches.(idx f) <- !pending @ watches.(idx f); pending := [])else set c.(0)endenddonedone;!okinlet undo mark = for i = !len - 1 downto mark do assign.(abs trail.(i)) <- 0 done; len := mark; qhead := mark inlet rec dpll () =if not (propagate ()) then falseelselet rec first v = if v > n then 0 else if assign.(v) = 0 then v else first (v + 1) inlet v = first 1 inif v = 0 then trueelse beginlet mark = !len inincr decisions;set (-v);dpll () || (undo mark; set v; dpll ()) || (undo mark; false)endinlet ok = List.for_all (fun l -> if value l = -1 then false else (if value l = 0 then set l; true)) !units inif ok && dpll () then Some (Array.copy assign) else None
Bit-vector circuits encoded as clauses.
(* Bit-blasting: a bit-vector is an array of SAT literals, leastsignificant bit first, and every operation is a circuit whose gatesare encoded as clauses (the Tseitin encoding). Each instance of thefunctor has its own solver. *)module type S = sigval s : Sat.tval clause : int list -> unitval var : int -> int arrayval add : int array -> int array -> int arrayval mul : int array -> int array -> int arrayval eq : int array -> int array -> intendmodule Make () = structlet s = Sat.create ()let fresh () = Sat.fresh slet clause = Sat.add s(* Constants 1 and 0 as literals. *)let tt = let v = fresh () in clause [ v ]; vlet ff = - tt(* g <-> a /\ b, g <-> a \/ b, g <-> a xor b *)let and_ a b = let g = fresh () in clause [ -g; a ]; clause [ -g; b ]; clause [ g; -a; -b ]; glet or_ a b = - (and_ (-a) (-b))let xor_ a b =let g = fresh () inclause [ -g; a; b ]; clause [ -g; -a; -b ]; clause [ g; -a; b ]; clause [ g; a; -b ]; glet var w = Array.init w (fun _ -> fresh ())let const w k = Array.init w (fun i -> if (k lsr i) land 1 = 1 then tt else ff)(* Ripple-carry addition modulo 2^w: a full adder per bit. *)let add a b =let carry = ref ff inArray.init (Array.length a) (fun i ->let s = xor_ (xor_ a.(i) b.(i)) !carry incarry := or_ (and_ a.(i) b.(i)) (and_ !carry (xor_ a.(i) b.(i)));s)(* Shift-and-add multiplication modulo 2^w: w partial products. *)let mul a b =let w = Array.length a inlet acc = ref (const w 0) infor i = 0 to w - 1 dolet partial = Array.init w (fun j -> if j < i then ff else and_ a.(j - i) b.(i)) inacc := add !acc partialdone;!acc(* Equality of two vectors as one literal, and unsigned a < b. *)let eq a b = let d = Array.map2 (fun x y -> - (xor_ x y)) a b in Array.fold_left and_ tt dlet ult a b =(* scan from the least significant bit: lt := (not a_i and b_i) or (a_i = b_i and lt) *)let lt = ref ff inArray.iteri (fun i x -> let y = b.(i) in lt := or_ (and_ (-x) y) (and_ (- (xor_ x y)) !lt)) a;!ltlet value model v = Array.fold_left (fun (acc, k) l -> ((if model.(abs l) = (if l > 0 then 1 else -1) then acc lor (1 lsl k) else acc), k + 1)) (0, 0) v |> fstend
Solving
A query asserts the output literal of a circuit and asks for a satisfying assignment, from which the values of the bit-vectors are read back. Asking for with finds a factorization:
Factoring 143 with a multiplier circuit, with and without overflow.
(* Factor 143 with multiplier circuits. Bit-vector multiplication ismodulo 2^w, so the first query finds a product that wraps around; thesecond zero-extends 8-bit operands to 16 bits so that it cannot. *)let query w ext =let module B = Bv.Make () inlet open B inlet x = var 8 and y = var 8 inlet widen v = if ext then Array.append v (const 8 0) else v inlet p = mul (widen x) (widen y) inclause [ eq p (const w 143) ];clause [ ult (const 8 1) x ];clause [ ult (const 8 1) y ];let nv = s.Sat.nvars and nc = List.length s.Sat.clauses inmatch Sat.solve s with| Some m -> Printf.printf "%2d-bit product: %4d variables, %5d clauses: x = %3d, y = %3d, x * y = %d\n" w nv nc (value m x) (value m y) (value m x * value m y)| None -> Printf.printf "%2d-bit product: unsatisfiable\n" w
Running it.
8-bit product: 517 variables, 1720 clauses: x = 129, y = 15, x * y = 193516-bit product: 1785 variables, 6108 clauses: x = 13, y = 11, x * y = 143
The 8-bit query finds : a correct answer to the question that was asked, since 8-bit multiplication wraps around. Zero-extending the operands to 16 bits before multiplying rules out overflow and gives the factorization . The distinction is the one a program verifier has to get right, and the kind of bug bit-precise reasoning is used to find.
Proving identities
A property holds for all inputs exactly when its negation is unsatisfiable. Commutativity of addition and multiplication at width is proved by asserting or and finding no model:
Proving commutativity by unsatisfiability.
(* x + y = y + x and x * y = y * x hold for all w-bit x and y exactly whentheir negations are unsatisfiable. *)let prove w name op =let module B = Bv.Make () inlet x = B.var w and y = B.var w inlet l = op (module B : Bv.S) x y and r = op (module B : Bv.S) y x inB.clause [ - (B.eq l r) ];Sat.decisions := 0;let result = Sat.solve B.s inPrintf.printf "%-3s w = %-2d %6d vars %7d clauses %s %9d decisions\n%!" name w B.s.Sat.nvars(List.length B.s.Sat.clauses) (if result = None then "proved " else "counterexample") !Sat.decisions
Running it with the DPLL solver above.
x+y w = 4 65 vars 198 clauses proved 255 decisionsx+y w = 6 97 vars 296 clauses proved 4095 decisionsx+y w = 8 129 vars 394 clauses proved 65535 decisionsx+y w = 10 161 vars 492 clauses proved 1048575 decisionsx*y w = 3 133 vars 437 clauses proved 42 decisionsx*y w = 4 229 vars 762 clauses proved 170 decisionsx*y w = 5 351 vars 1177 clauses proved 682 decisionsx*y w = 6 499 vars 1682 clauses proved 2730 decisionsx*y w = 7 673 vars 2277 clauses proved 10922 decisions
Without clause learning, the solver proves the adder identity by trying every combination of inputs, decisions, since it branches on the input bits first and only finds a conflict once all of them are set. A CDCL solver learns clauses that summarize why each carry must agree and proves adder identities for any practical width almost immediately. Multipliers are different: equivalence of two multiplier circuits, even commutativity of one, remains hard for CDCL solvers even at moderate widths, and dedicated methods based on computer algebra are used to verify large multipliers[7]. Binary decision diagrams have the same difficulty: Bryant proved that every BDD for the middle output bit of a multiplier has exponential size[8].
In SMT solvers
Solvers do not bit-blast a formula as written. Word-level preprocessing first rewrites it: constant propagation, normalization of arithmetic, elimination of variables defined by equations, and simplifications such as replacing multiplication by a constant with shifts and additions. What remains is bit-blasted, eagerly, all at once, or lazily, with parts of the formula handled at the word level and blasted only when needed[6]. STP[3] and Boolector[4] pioneered this for program analysis; Z3[5], cvc5 and Bitwuzla are current examples. Because the result is a SAT problem, bit-vector solvers inherit every improvement in SAT solving.
Uses
Symbolic execution tools such as KLEE represent program values as bit-vector expressions and ask a solver for inputs that reach each branch[9]. Bounded model checking unrolls a hardware or software transition system for a fixed number of steps and bit-blasts the result into one SAT query[10]. Equivalence checking of circuits, superoptimization of instruction sequences, translation validation in compilers and the analysis of cryptographic code all reduce to bit-vector queries.
History
Reducing arithmetic on machine words to propositional logic is as old as circuit design, and Tseitin's encoding of 1968 made the translation to clauses linear[2]. Bounded model checking made bit-blasted SAT queries a mainstream verification technique in 1999[10]. Ganesh and Dill's STP of 2007 and Brummayer and Biere's Boolector of 2009 showed that word-level preprocessing followed by bit-blasting could handle the formulas produced by program analysis[3][4], and bit-vectors became one of the most used theories of SMT-LIB[11].
see also
- SATThe Boolean satisfiability problem: given a propositional formula, decide whether some assignment of true and false to its variables makes it true. It is the canonical NP-complete problem, and also one that modern solvers routinely settle for instances with millions of variables.
- Tseitin transformationA translation of a propositional formula into CNF that introduces a fresh variable for each subformula and adds clauses saying the variable equals it. The result is equisatisfiable with the input rather than equivalent, and its size is linear in the input's.
- SMTSatisfiability modulo theories: SAT where the atoms are not opaque booleans but statements in some theory, such as linear arithmetic or equality over uninterpreted functions. The formula must be satisfiable both propositionally and in the theory, which is what makes SMT the engine under most program verifiers.
- CNFConjunctive normal form: a conjunction of clauses, each a disjunction of literals, where a literal is a variable or its negation. It is the input format of SAT solvers. Every formula has an equivalent CNF, but it can be exponentially larger; the Tseitin transformation gives an equisatisfiable one of linear size instead.
- CDCLConflict-driven clause learning: DPLL plus the observation that a conflict is a proof that some set of decisions is impossible. The solver analyses the implication graph, derives a new clause ruling that set out, adds it to the database, and backjumps possibly many levels at once rather than undoing one decision at a time.
further reading
- [1]D. Kroening, O. Strichman, Decision Procedures: An Algorithmic Point of View, ch. 6, Springer (2nd ed., 2016).
- [2]G. S. Tseitin, “On the complexity of derivation in propositional calculus”, Studies in Constructive Mathematics and Mathematical Logic, Part II (1968).
- [3]V. Ganesh, D. L. Dill, “A decision procedure for bit-vectors and arrays”, Computer Aided Verification, LNCS 4590 (2007).
- [4]R. Brummayer, A. Biere, “Boolector: an efficient SMT solver for bit-vectors and arrays”, TACAS, LNCS 5505 (2009).
- [5]L. de Moura, N. Bjørner, “Z3: an efficient SMT solver”, TACAS, LNCS 4963 (2008).
- [6]L. Hadarean, K. Bansal, D. Jovanović, C. Barrett, C. Tinelli, “A tale of two solvers: eager and lazy approaches to bit-vectors”, Computer Aided Verification, LNCS 8559 (2014).
- [7]D. Kaufmann, A. Biere, M. Kauers, “Verifying large multipliers by combining SAT and computer algebra”, Formal Methods in Computer-Aided Design (2019).
- [8]R. E. Bryant, “On the complexity of VLSI implementations and graph representations of Boolean functions with application to integer multiplication”, IEEE Transactions on Computers 40 (1991).
- [9]C. Cadar, D. Dunbar, D. Engler, “KLEE: unassisted and automatic generation of high-coverage tests for complex systems programs”, OSDI (2008).
- [10]A. Biere, A. Cimatti, E. Clarke, Y. Zhu, “Symbolic model checking without BDDs”, TACAS, LNCS 1579 (1999).
- [11]C. Barrett, P. Fontaine, C. Tinelli, The SMT-LIB Standard: Version 2.6 (2017).
last updated