Datalog-based Scalable Semantic Diffing of Concurrent Programs

07/10/2018
by   Chungha Sung, et al.
0

When an evolving program is modified to address issues related to thread synchronization, there is a need to confirm the change is correct, i.e., it does not introduce unexpected behavior. However, manually comparing two programs to identify the semantic difference is labor intensive and error prone, whereas techniques based on model checking are computationally expensive. To fill the gap, we develop a fast and approximate static analysis for computing synchronization differences of two programs. The method is fast because, instead of relying on heavy-weight model checking techniques, it leverages a polynomial-time Datalog-based program analysis framework to compute differentiating data-flow edges, i.e., edges allowed by one program but not the other. Although approximation is used our method is sufficiently accurate due to careful design of the Datalog inference rules and iterative increase of the required data-flow edges for representing a difference. We have implemented our method and evaluated it on a large number of multithreaded C programs to confirm its ability to produce, often within seconds, the same differences obtained by human; in contrast, prior techniques based on model checking take minutes or even hours and thus can be 10x to 1000x slower.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

10/10/2017

Causality-based Model Checking

Model checking is usually based on a comprehensive traversal of the stat...
08/07/2018

A Spin-based model checking for the simple concurrent program on a preemptive RTOS

We adapt an existing preemptive scheduling model of RTOS kernel by eChro...
08/21/2020

Transforming Probabilistic Programs for Model Checking

Probabilistic programming is perfectly suited to reliable and transparen...
09/20/2017

CARET analysis of multithreaded programs

Dynamic Pushdown Networks (DPNs) are a natural model for multithreaded p...
08/06/2019

Does Preliminary Model Checking Help With Subsequent Inference? A Review And A New Result

Statistical methods are based on model assumptions, and it is statistica...
09/27/2019

LTL Model Checking of Self Modifying Code

Self modifying code is code that can modify its own instructions during ...
08/02/2018

Optimal Stateless Model Checking under the Release-Acquire Semantics

We present a framework for efficient application of stateless model chec...
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1. Introduction

When an evolving concurrent program is modified, often times, the sequential program logic is not changed; instead, the modification focuses on thread synchronization, e.g., to optimize performance or remove bugs such as data-races and atomicity violations. Since concurrency is hard, it is important to ensure the modification is correct and does not introduce unexpected behavior. However, manually comparing two programs to identify the semantic difference is difficult, and the situation is exacerbated in the presence of thread interactions: changing a single instruction in a thread may have a ripple effect on many instructions in other threads. Although techniques have been proposed to compute the synchronization difference, e.g., by leveraging model checkers (Bouajjani17, ), they are expensive for practice use. For example, comparing two versions of a program with 578 lines of C code takes half an hour.

To fill the gap, we develop a fast and approximate static analysis to compute such differences with the goal of reducing analysis time from hours or minutes to seconds. We assume the two programs are closely related versions of an evolving software where changes are made to address issues related to thread synchronization as opposed to the sequential computation logic. Therefore, same as in prior works (ShashaS88, ; Bouajjani17, ), we focus on synchronization differences. However, our method is orders-of-magnitude faster because instead of model checking we leverage a polynomial-time declarative program analysis framework which uses a set of Datalog rules to model and reason about thread interactions.

The reason why prior techniques are expensive is because they insist on being precise. Specifically, they either enumerate interleavings or use a model checker to ensure a semantic difference, represented as a set of data-flow edges, is allowed by one of the programs but not by the other. However, this in general is equivalent to program verification, which is an undecidable problem (Ramalingam00, ); even in cases where it is reduced to a decidable problem, the cost of model checking is too high. Our insight is that in practice, it is relatively easy for developers to inspect a given difference to determine if it is feasible; what is not easy and hence requires tool support is a systematic exploration of behaviors of the two programs to identify all possible differences in the first place. Unfortunately, developing such a tool is a non-trivial task; for example, the naive approach of comparing individual thread interleavings would not work due to the often exponential blowup in the number of interleavings.

Our method avoids the problem by being approximate in that it does not enumerate interleavings. This also means infeasible behaviors are sometimes included. However, our approximation is carefully designed to take into consideration the program semantics most relevant to thread interaction. Furthermore, the approximation can be refined by iteratively increasing the number of data-flow edges used to characterize a synchronization difference. We shall show through experiments that our fast and approximate analysis method does not lead to overly inaccurate results. To the contrary, the synchronization differences reported by our method closely match the ones identified by human. Compared to the prior technique based on model checking, which often takes minutes or even hours, our method can be 10x to 1000x faster.

Figure 1. Overview of our semantic diffing method.

Figure 1 shows the overall flow of our method. The input consists of two versions of a concurrent program: is the original version, is the changed version, and patch info represents their syntactic difference, e.g., information about which instructions are added, removed or modified. The output consists of a set of differences, each of which is represented by a set of data-flow edges allowed in one of the programs but not the other. When data-flow edges are allowed in but not , for example, they represent a removed behavior. Conversely, when data-flow edges are allowed in but not , they represent a new behavior introduced by the change.

Our method first generates a set of Datalog facts that encode the structural information of the control flow graphs. These facts are then combined with inference rules that codify the analysis algorithm. When the combined program is fed to a Datalog solver, the resulting fixed point contains new relations (facts) that represent the analysis result. Specifically, it contains data-flow edges that may occur in each program. By comparing data-flow edges from the two programs, we can identify the semantic differences.

Since program verification is undecidable in general, and with concurrency, verification is undecidable even for Boolean programs (Ramalingam00, ), approximation is inevitable. Our method makes two types of approximations. The first one is in checking the feasibility of data-flow edges. The second one is related to the number of data-flow edges used to characterize a difference, also referred to as the rank of an analysis (Bouajjani17, ). Although in the worst case, a precise analysis means the rank needs to be as large as the length of the execution, we restrict it to a small number in our method because prior research (MusuvathiQBBNN08, ; BindalBL13, ) shows that concurrency bugs often can be exposed by executions with a bounded number of context switches.

Since our method is approximate in nature, the usefulness depends on how close it approaches the ground truth. Ideally, we want to have few false positives and few false negatives. Toward this end, we choose to stay away from the tradition of insisting the analysis being either sound or complete when one cannot have both. For a concurrent program, being sound often means existential abstraction: a data-flow edge is considered feasible (in all interleavings) if it is feasible in an interleaving, and being complete often means universal abstraction: a data-flow edge is considered feasible only if it is feasible in all interleavings. Both cases result in extremely coarse-grained approximations, which in turn lead to numerous false positives or false negatives. Instead, we want to minimize the difference between our analysis result and the ground truth.

We have implemented our method in a tool named EC-Diff, which uses LLVM (Adve03, ) as the front-end and  (Hoder11, ) in Z3 as the Datalog solver. We evaluated EC-Diff on 47 multithreaded programs with 13,500 lines of C code in total. These are benchmarks widely used in prior research (Beyer15, ; Bloem14, ; WangYGG08, ; Lu08, ; Yu09, ; YangCGW09, ; Yin11, ; llvm8441, ; gcc25530, ; gcc21334, ; gcc40518, ; gcc3584, ; glib512624, ; jetty1187, ; Herlihy08, ): some illustrate real concurrency bug patterns (Yu09, ) and the corresponding patches (Khoshnood15, ) while others are applications from public repositories. We applied EC-Diff to these benchmarks while comparing with the prior technique of Bouajjani et al. (Bouajjani17, ). Our results show that EC-Diff can detect, often in seconds, the same differences identified by human. Furthermore, compared to the prior technique based on model checking, EC-Diff is 10x to 1000x faster.

To summarize, this paper makes the following contributions:

  • We propose a fast and approximate analysis based on a polynomial-time declarative program analysis framework to compute synchronization differences.

  • We show why our approximate analysis is reasonably accurate due to the custom-designed inference rules and iterative increase of the number of data-flow edges.

  • We implement our method in a practical tool and evaluate it on a large number of benchmarks to confirm its high accuracy and low overhead.

The remainder of the paper is as follows. First, we motivate our work using examples in Section 2. Then, we provide the technical background in Section 3 before presenting our analysis method in Section 4. This is followed by our procedures for interpreting the analysis result and optimizing performance in Section 5. We present our experimental results in Section 6. Finally, we review the related work in Section 7 and give our conclusions in Section 8.

2. Motivation

We use examples to motivate the need for conducting a differential analysis. Programs used in these examples illustrate common bug patterns (also used during our experiments in Section 6). In each example, there are two program versions: the original one may violate a hypothetical assertion and the changed one avoids it. These assertions are hypothetical (added for illustration purposes only) in the sense that our method does not need them to operate.

2.1. The First Example

Fig. 2(a) shows a two-threaded program where the shared variable x is initialized to 0. The assertion at Line 3 may be violated, e.g., when thread1 executes the statement at Line 2 right after thread2 executes the statement at Line 5. The reason is because no synchronization operation is used to enforce any order.

Assume the developer identifies the problem and patches it by adding locks (Figure 2(b)), the assertion violation will be avoided. To see why this is the case, consider the data-flow edge from Line 5 to Line 2: due to the critical sections enforced by lock-unlock pairs, the load of x at Line 2 is not affected by the store of x at Line 5. For example, if the critical section containing Line 5 is executed first, the subsequent unlock(a) must be executed before the lock(a) in thread1, which in turn must be executed before Line 1 and Line 2. Since the store of x at Line 1 is the most recent, the load of x at Line 2 will get its value, not the value written at Line 5.

thread1 {

1:  x = x + 1;

2:  if (x == 0)

3:       assert(0);

}

thread2 {       

4:  x = 1;

5:  x = 0;

}

RF

RF
(a) Before change

thread1 {

lock(a);

1:  x = x + 1;

2:  if (x == 0)

3:       assert(0);

unlock(a);

}

thread2 {       

4:  x = 1;

lock(a);

5:  x = 0;

unlock(a);

}

RF

RF
(b) After change
Figure 2. Example programs with synchronization differences (lock-unlock).

Thus, the allowed data-flow edges are as follows: RF(L4,L2) and RF(L5,L2) for the original program, and RF(L4,L2) for the changed program. This notion of comparing concurrent executions was introduced by Shasha and Snir (ShashaS88, ) and extended by Bouajjani et al. (Bouajjani17, ), although in both cases, enumeration or model checking techniques were used. In our work, the goal is to avoid such heavyweight analyses while maintaining sufficient accuracy.

In addition to RF edges, there are other types of relations considered during our analysis, including program order, inter-thread order imposed by thread create, join, signal-wait as well as store-store order. Nevertheless, when interpreting the final results, we focus on differences in the RF edges because they affect the externally observable behavior of a program, e.g., characterized by assertions and other reachability properties.

2.2. The Second Example

Fig. 3 shows a more sophisticated example: the use of signal-wait, which is often difficult for static analyzers. Since the variable x is initialized to 0, when the critical section in thread1 is executed before thread2, the load of x at Line 1 will get the value 0, which leads to the assertion violation in Fig. 3(a). Assume the intended behavior is for thread2 to complete first, an inter-thread execution order must be enforced, e.g., by using the signal-wait pair shown in Fig. 3(b). The assertion violation is avoided because the load of x at Line 1 can only read from the store of x at Line 5.

To correctly deploy the signal-wait pair, a variable named cBool needs to be added. If the operating system voluntarily schedules thread2 first, thread1 needs to be aware – by checking the value of cBool – and then skips the execution of wait; otherwise, wait may get stuck because the corresponding signal has already been fired (and lost). But if thread1 is executed first, since cBool has not been set, it will invoke wait which forces the corresponding signal to be sent.

As for the data-flow edges, we can see that RF(L5,L1) and RF(L3,L4) are allowed in the original program, but only RF(L5,L1) is allowed in the changed program. RF(L3,L4) is not allowed because Line 4 must happen before Line 5, Line 5 must happen before signal, and signal must happen before wait, which resides before Lines 1-3 in thread1. Thus, there is a cycle (contradiction).

thread1 {

lock(a);

1:  if (x == 0)

2:       assert(0);

3:  y = foo(x);

unlock(a);

}

thread2 {       

lock(a);

4:  bar(y);

5:  x = 4;

unlock(a);

}

RF

RF
(a) Before change

thread1 {

lock(a);

if (!cBool)

wait(cond);

1:  if (x == 0)

2:       assert(0);

3:  y = foo(x);

unlock(a);

}

thread2 {       

lock(a);

4:  bar(y);

5:  x = 4;

cBool = 1;

signal(cond);

unlock(a);

}

RF

RF
(b) After change
Figure 3. Example programs with synchronization differences (signal-wait).

2.3. How Our Method Works

Our method differs from prior techniques which rely on either enumerating interleavings and conducing pairwise comparison (ShashaS88, ), or model checking based techniques (Bouajjani17, ). Both are computationally expensive. Instead, we use lightweight static analysis.

Our method represents the control and data dependencies of each program as a set of Datalog facts. We also design a set of Datalog inference rules, which capture our algorithm for deriving new facts from existing facts. Leveraging a Datalog solver, we can repeatedly apply the inference rules over the facts until a fixed point is reached. We will explain details of our Datalog facts and inference rules in Section 4.

Fig 2(a) MayRF Fig 2(b) MayRF
Fig 3(a) MayRF Fig 3(b) MayRF
Figure 4. Analysis steps for programs in Figs. 3(a) and 3(b).

For now, consider the steps of computing synchronization differences for the programs in Fig. 2 and Fig. 3, which are outlined by the tables in Fig. 4.

First, our method computes must-happen-before (mustHB) edges, which represent the execution order of two instructions respected by all thread interleavings. From mustHB, our method computes may-happen-before (mayHB) edges, which represent the execution order respected by some interleavings, e.g., thread context switches not contradicting to mustHB. From mayHB, our method computes MayRF edges, which represent data flows (over shared variables) from store instructions to the corresponding load instructions.

The MayRF edges are over-approximated in that, if an edge is included in MayRF, the corresponding data flow may occur in an execution. But if an edge is not included in MayRF, we know for sure the corresponding data flow is definitely infeasible. For example, in Fig. 4, MayRF has four edges for Fig. 2(a) but only three edges for Fig. 2(b). RF(L5,L2) is no longer allowed in the changed program, indicating it is a difference between the two programs.

For the example in Fig. 3, we compute mustHB based on the sequential program order and, in Fig. 3(b), the inter-thread execution order imposed by signal-wait. Then, from mustHB we compute mayHB, which includes edges in mustHB and more. For Fig. 3(a), since there is no restriction on the inter-thread execution order, all pairs of events are included, whereas for Fig. 3(b), there is only one-way data flow. Finally, we compute MayRF based on mayHB. There are three edges for Fig. 3(a) but only two for Fig. 3(b).

2.4. The Rank of an Analysis

When comparing MayRF in these two examples, we identify the difference as edges allowed in only one of the two programs, such as RF(L5,L2)in Fig. 2 and RF(L3,L4) in Fig. 3.

However, even if MayRF edges are allowed individually, they may not occur in the same execution. For example, RF(L5,L1) and RF(L3,L4) in Fig. 3(a) cannot occur together because, otherwise, they form a cycle together with the program order edges. Our method has inferences rules designed to check if two or more data-flow edges can occur together—this is referred to as the rank (Bouajjani17, ).

With the notion of rank, we can capture ordered sets of MayRF edges, as opposed to individual MayRF edges. Thus, even if the MayRF relation remains the same, there may be differences of high ranks: two or more edges from MayRF may occur together in but not in . We will present our method for checking such differences in Section 5 following the baseline procedure in Section 4.

3. Preliminaries

3.1. Partial trace comparison

To compare the synchronizations of two concurrent programs, we use the notion of partial trace introduced by Shasha and Snir (ShashaS88, ) and extended by Bouajjani et al. (Bouajjani17, ). Let be a program and be the set of global variables shared by threads in . For each , let denote a store instruction and denotes a load instruction. Let be the set of all instructions in the program. Any binary relation over these instructions is a subset of .

For example, is a relation that orders the store instructions; means   is executed before . Thus, in Fig. 2(a), (L1,L4), (L4,L1), (L1,L5), (L5,L1), (L4,L5) belong to , but (L5,L4) does not belong to because it is not consistent with the program order.

Similarly, is a relation between load and store instructions. In Fig. 2(a), we have (L4,L2) and (L5,L2) in , meaning the load at Line 2 may read from values written at Lines 4 and 5. Given and , we define as a set of subsets of , where each element has at most edges.

Edges in are from either or – they capture the abstract trace. The number , which is called the rank (Bouajjani17, ), is bounded by the length of the trace.

Definition 0 (Abstract Trace with Rank ).

An abstract trace with rank is a tuple , where , , and .

Given the abstract traces and of two programs and , respectively, we define their difference as , where and . Next, we define what it means for to be a refinement of , denoted .

Definition 0 (Abstract Trace Refinement).

Given two abstract traces and , we say is a refinement of , denoted , if and only if , , and .

That is, when , the abstract behavior of is covered by that of . And the difference () is characterized by , , and . Finally, if the abstract traces of and refine each other, we say they are rank- equivalent.

Although comparison of abstract traces involves and , when reporting the differences, we focus on the edges only because they directly affect the observable behaviors of the programs. In contrast, store-store ordering () may not be observable unless they also affect the read-from () edges.

3.2. Datalog-based Analysis

Datalog is a logic programming language but in recent years has been widely used for declarative program analysis 

(Dawson96, ; Whaley04, ; Livshits05, ; Hajiyev06, ; Heintze01, ; Bravenboer09, ; ZhangMGNY14, ; AlbarghouthiKNS17, ). The main advantage is that a Datalog program is polynomial-time solvable and the corresponding fixed-point computation maps naturally to fixed-point computations in program analysis algorithms. In this context, structural information of the program is represented as relations called the facts, while the fixed-point algorithm is expressed as recursive relations called the inference rules.

Consider a relation named PO, which represents the program order of two immediate adjacent instructions and , while HB means must happen before . First, we write down the Datalog facts based on the CFG structure:

, , , , .

Then, we write down the Datalog inference rules:

Here, the left arrow () separates the inferred Datalog facts on the left-hand side from the existing Datalog fact(s) on the right-hand side. The first rule says the program-order relation implies the must-happen-before relation. The second rule says the must-happen-before relation is transitive.

A Datalog solver, based on the above facts and rules, will compute the maximal set of edges for the HB relation. By sending a query to the Datalog solver, one may confirm that indeed holds whereas does not hold.

4. Constraint-based Synchronization Analysis

In this section, we present our method for computing abstract traces of a single program. In the next section, we leverage the abstract traces of two programs to compute their differences.

First, we define the elementary relations that can be constructed directly from the CFG of a program.

  • : Statement resides in Thread

  • : Statement is before in a thread

  • : Statement dominates in a thread

  • : post-dominates in a thread

  • : Thread creates at

  • : Thread joins back at

  • : waits for condition variable

  • : sends condition variable

  • : Statement reads from variable

  • : Statement writes to variable

  • : resides in a critical section guarded by lock()–unlock() pair

  • : and are in the same critical section guarded by

  • : and are in different critical sections guarded by

While traversing the CFG to compute the Po, Dom, and PostDom relations, we take loops into consideration. For example, two instructions involved with the same loop may not have a Dom or PostDom relation, but an instruction outside the loop can have a Dom or PostDom relation with an instruction inside the loop.

Next, we define inference rules for computing new relations such as MayHb, MustHb, and MayRf.

4.1. Rules for Intra-thread Dependency

To capture the execution order of instructions, we define the following relations: means may happen before in some execution, and means happens before in all executions when both occur. Since the program order in each thread implies the execution order, we have the following rule:

In this work, we assume sequential consistency but Datalog is capable of handling weaker memory models (KusanoW17, ) as well.

By definition MustHb implies MayHb, which means

4.2. Rules for Inter-thread Dependency

When a parent thread creates a child thread at the statement , e.g., by invoking pthread_create, any statement in the child thread must occur after .

Similarly, when a parent thread joins back a child thread at , any statement in must occur before .

4.3. Rules for Signal-Wait Dependency

When a condition variable is used, e.g., through signal(c) and wait(c), it imposes an execution order.

However, the rule needs to be used with caution. In practice, wait(c) is often wrapped in an if-condition as shown in Figure 3(b). To be conservative, our method analyzes the control flow of these threads and applies the above rule only after detecting the usage pattern. Since our method does not analyze the concrete values of any shared variables, it does not check if the if-condition is valid. Also, developers may use condition variables in a different way. Thus, in our experiments (Section 6), we evaluated the impact of this conservative approach—assuming the if-condition is always valid—to confirm it does not lead to significant loss of accuracy.

4.4. Ad Hoc Synchronization

We handle ad hoc synchronization similar to signal-wait. Fig. 5 shows an example where cond is a user-added flag initialized to 0. The busy-waiting in thread2 ensures that a=1 always occurs before x=a. By traversing the CFGs of these threads, we can identify the pattern; this is practical since the number of usage patterns is limited. After that, we add a MustHb edge from cond=true to while(!cond). This is similar to adding MustHb edges for CondWait and CondSignal. As a result, we can decide the read-from edge between x=a and the initialization of a is infeasible.

thread1() { a = 1; cond = true; }         thread2() { while(!cond) {} x = a; }
Figure 5. Ad hoc synchronization (cond = false initially).

4.5. Transitive Closure

Since MustHb is transitive, we use the following rule to compute the transitive closure:

When instructions in concurrent threads are not ordered by MustHb, we assume they may occur in any order:

The MayHb relation is also transitive:

4.6. Lock-enforced Critical Section

For critical sections based on lock-unlock, we introduce rules based on access patterns. First, we compute , meaning the store in is overwritten by a subsequent store in the same critical section. Consider lk(a) (v) (v) unlk(a), where (v) is a covered store and thus not visible to reads in other critical sections protected by the same lock.

Similarly, CoveredLoad means the load of in is covered and thus can only read from a preceding store in the same critical section.

Consider lk(a) W(v) R(v) unlk(a) as an example: R(v) is covered by W(v) and thus cannot read from stores in other critical sections protected by the same lock.

4.7. Read-from Relation

Finally, we compute NoRf which means the read-from edge between and is infeasible.

That is, in , the first store cannot be read by the load. In addition to this generic rule, we have two more inference rules:

This rule means if one store may happen before one load, the load is covered, and the store is in a different critical section, the load cannot read from the store. This is because another store will overwrite the value to be read.

This rule means if a store is covered, i.e., overwritten by a subsequent store, the store cannot reach to any load in other critical sections protected by the same lock.

We also compute MayRf which means the load in may read from the store in .

5. Computing the Differences

In this section, we show how to compare abstract traces of the two programs to identify the differences.

5.1. Symmetric Difference

Fig. 6 shows the Venn diagram of our method for computing the differences when given the abstract traces of two programs. The actual behaviors of programs and are represented by the circles with solid lines. The approximate behaviors, in the form of abstract traces and , are represented by the circles with dashed lines. Conceptually, the symmetric difference is computed based on and , and for each is presented as pink-colored region in Fig. 6 (left and right). The details of them are presented in the remainder of this section.

Figure 6. Differences of abstract traces: (left) and (right).

To compute the difference, we define two relations DiffP1 and DiffP2 and rules for computing them:

DiffP1 represents edges that may happen in but not in . Similarly, DiffP2 represents edges that may happen in but not in . If DiffP1 is not empty, there are more behaviors in ; and if DiffP2 is not empty, there are more behaviors in .

Since the Datalog solver may enumerate all possible MayHb edges (used to compute MayRf), and the number of MayHb edges increases rapidly as the program size increases, we need to reduce the computational overhead. Our insight is that, since we are only concerned with synchronization differences in the end, as opposed to behaviors of the sequential computation, we can restrict our analysis to instructions that access global variables. Toward this end, we define a new relation named which means accesses a global variable , and use it to guard the inference rules for MayHb (and hence MustHb). It forces the Datalog solver to consider only global accesses, which reduces the computational overhead without losing accuracy. We demonstrate the effectiveness of this optimization using experiments in Section 6.

5.2. Differences at Higher Ranks

The rules so far use individual read-from edges to characterize the differences, which is equivalent to rank-1 analysis (Bouajjani17, ), but some programs may not have rank-1 difference but have differences of higher ranks. To detect them, we need to compute ordered sets of data-flow edges allowed in one program but not in the other.

To be specific, for rank-2, we extend the MayRf relation, which was defined over two instructions (an edge), to MayRfs defined over four instructions, to represent an ordered set of (two) read-from edges. Similarly, we extend the NoRf relation to NoRfs, which is also defined over four instructions to represent an ordered set of (two) read-from edges.

Previously, means there is no execution trace where the store can be read by the load , whereas means there may exist some execution trace that allows the read-from edge . Similarly, means there is no execution trace where the two read-from edges and occur together and in that order; and means there may exist some execution trace that allows the two read-from edges to occur together and in that order.

:W(x)

:W(x)

:R(x)

RF

RF

:R(x)

:W(x)

:R(x)

:W(x)

RF

RF

MustHB

MustHB
Figure 7. Illustrating the first two rank-2 inference rules.

First, we present our rules for computing NoRfs, which in turn is used to compute MayRfs. Since it is not possible to enumerate all scenarios due to theoretical limitations, we resort to the most common scenarios. Nevertheless, we guarantee that NoRfs is an under-approximation, and the corresponding MayRfs is an over-approximation.

This rule is obvious because, as in Fig. 7 (left), in the same execution trace a load () cannot read from two different stores ().

This rule is also obvious because, as shown in Fig. 7 (right), if the two read-from edges form a cycle together with the must-happen-before edges, they lead to a contradiction.

This rule is related to lock-unlock pairs. The rationale behind it can be explained using the diagram in Fig. 8 (left). Due to the lock-unlock pairs, there are only two possible interleavings: (1) if happens before , must happen before and , which contradicts to the read-from edge ; (2) if happens before , must happen before , which contradicts to the read-from edge . Thus, the read-from edges cannot occur in the same execution trace.

lk()

:W(x)

:R(x)

unlk()

lk()

:R(x)

:W(x)

unlk()

RF

RF

lk()

:R(x)

unlk()

:W(x)

lk()

:R(x)

:W(x)

unlk()

RF

RF

PostDom
Figure 8. Illustrating rank-2 rules related to lock-unlock.

Next, we define another rule related to lock-unlock pairs. In this rule, we use PostDom to mean, after is executed, is guaranteed to be executed as well.

The rationale behind this rule can be explained using the diagram in Fig. 8 (right). Here, the loads and stores access the same variable. If the read-from edge is ahead of in the same execution trace, the store in contradicts to the read-from edge .

Finally, we compute MayRFs based on NoRFs:

It means the read-from edges () and () may occur together and in that order in some execution trace. With MayRFs, we compute differences (DiffP1 and DiffP2) by replacing MayRf with MayRFs. Our method for computing differences of rank 3 or higher are similar, and we omit the details for brevity.

5.3. Example for Rank-2 Analysis

Fig. 9 shows an example that illustrates the rank-2 analysis. Here, thread1 sets t to 0 and x to 1 before creating thread2. Due to lock-unlock pairs, the assertion cannot be violated in Fig. 9(a). However, if the lock-unlock in thread1 is removed as in Fig. 9(b), the assertion may be violated because, in between Lines 4 and 5, there may be a context switch which was not allowed previously.

However, the synchronization difference cannot be captured by any individual MayRF edge. In fact, the table in Fig. 10 shows that the two programs have the same set of MayRF edges. In particular, since there are two stores of x, the load at Line 2 may read from both Line 1 and Line 5.

To capture the difference, we need rank-2 analysis.

  • Assume RF(L1,L4) occurs first, meaning thread2 acquires the lock and thus prevents thread1 from acquiring the same lock until thread2 exits the critical section. It means the store at Line 5 will set x to 2. Therefore, the load of x at Line 2 will have to read from Line 5, not from Line 1. In other words, RF(L1,L2) cannot occur after RF(L1,L4) in the same execution.

  • Assume RF(L1,L2) occurs first and thread2 will not be executed until thread1 finishes. In this case, RF(L1,L4) is allowed since no store of x is in thread1.

As a result, the program in Fig. 9(a) allows the ordered set {RF(L1,L2), RF(L1,L4)} but not the ordered set {RF(L1,L4), RF(L1,L2)}.

However, the program in Fig. 9(b) allows the ordered set {RF(L1,L4), RF(L1,L2)} as well, due to the removal of the lock-unlock pairs in thread1. Specifically, when RF(L1,L4) occurs at the start of an execution, thread1 may execute Line 2 before thread2 execute Line 5, which allows Line 2 to read the value of x from Line 1.

Our steps of conducting the rank-2 analysis, based on inference rules presented so far, are shown in Fig. 10. There is no difference in the MayRF sets; however, when comparing the ordered set of MayRF edges, we can still see the difference. To support this analysis, we apply the aforementioned inference rules of rank 2, which checks the existence of .

thread1 {

t = 0;

1:  x = 1;

create(t2);

lock(a);

2:  assert(x != t);

unlock(a);

}

thread2 {       

lock(a);

4:  t = x;

5:  x = 2;

unlock(a);

}

1
:RF

2
:RF

RF
(a) Before change

thread1 {

t = 0;

1:  x = 1;

create(t2);

lock(a);

2:  assert(x != t);

unlock(a);

}

thread2 {       

lock(a);

4:  t = x;

5:  x = 2;

unlock(a);

}

1
:RF

HB

2
:RF
(b) After change
Figure 9. Example programs with rank-2 differences.
Fig 9(a) MayRF Rank2 Fig 9(b) MayRF Rank2
Figure 10. Steps of our analysis for the programs in Fig. 9.

6. Experiments

We have implemented the method in a tool named EC-Diff, which uses LLVM (Adve03, ) as the frontend and  (Hoder11, ) in Z3 as the Datalog solver at the backend. Specifically, we use Clang/LLVM to parse the C/C++ code of multithreaded programs and construct the LLVM intermediate representation (IR). Then, we traverse the LLVM IR to generate program-specific Datalog facts. These Datalog facts, when combined with a set of program-independent inference rules, form the entire Datalog program. Finally, the Datalog solver is used to solve the program, which repeatedly applies the rules to the fact until a fixed point is reached. By querying relations in the fixed point, we can retrieve the analysis result.

6.1. Experimental Setup

We used two sets of benchmarks in our experiments. The first set of benchmarks consists of 41 multithreaded programs, which previously (Khoshnood15, ) have been used to illustrate concurrency bug patterns found in real applications (Beyer15, ; Bloem14, ; Lu08, ; Yu09, ; Yin11, ; llvm8441, ; gcc25530, ; gcc21334, ; gcc40518, ; gcc3584, ; glib512624, ; jetty1187, ; Herlihy08, ). With these programs, our goal is to evaluate how well the various types of concurrency bugs are handled by our method, and how our results compare to that of the prior technique based on model checking (Bouajjani17, ). For these benchmarks, the prior technique is not able to soundly instrument all applications. Therefore, we manually insert assertions to be checked later by the CBMC bounded model checker for detecting only one different edge.

The second set of benchmarks consists of 6 medium-sized applications from open-source repositories; they have also been used previously (Yang08, ; Yu09, ) to evaluate testing and automated program repair tools. Similarly, we are not able to apply the prior technique (Bouajjani17, ) because it has limitations to instrument large size programs and it is impossible for us to manually insert assertions. Nevertheless, we can evaluate how efficient our new method EC-Diff is on these real applications. In total, our benchmarks has 13,500 lines of C code.

For each benchmark program, there are two versions, one of which is the original program and the other is the changed program. These changed programs are patches collected from various sources: some are from benchmarks used in prior research on testing (Yu09, ; Yang08, ) and repair (Khoshnood15, ), whereas others are from benchmarks used in differential analysis (Bouajjani17, ). We also created four programs, case1-4, to illustrate motivating examples used throughout this paper. These benchmark programs, together with our experimental data, the LLVM-based tool, as well as data obtained from applying the prior technique (Bouajjani17, ), have been made available online111https://github.com/ChunghaSung/EC-Diff.

Our experiments were designed specifically to answer the following research questions:

  • Is our new method, based on a fast and approximate static analysis as opposed to heavy-weight model checking techniques, accurate enough for identifying the actual synchronization differences in the benchmark programs?

  • Is our new method significantly more efficient, measured in terms of the analysis time, than the prior technique based on model checking?

In all these experiments, we used a computer with an Intel Core i5-4440 CPU @ 3.10 GHz x 4 CPUs with 12 GB of RAM, running the Ubuntu-16.04 LTS operating system.

6.2. Results on the First Set of Benchmarks

Table 1 shows our results on the first set of benchmarks, with 41 programs illustrating common bug patterns. Columns 1 and 2 show the name and the number of lines of C code. Column 3 shows the number of threads. Column 4 shows the type of bug illustrated by the program. Specifically, Sync. means the bug is due to misuse of locks, and thus to repair it, some lock-unlock pairs have been added, removed or modified; Cond. means the bug is due to misuse of condition variables, and thus to repair it, some signal-wait pairs have been added, removed or modified; Th.Order means the bug is related to thread creation and join and thus involves ThrdJoin or ThrdCreate; and Order. means the bug is related to ordering of instructions imposed by ad-hoc synchronization. Note that, in each of these benchmarks, there is some synchronization difference.

The remaining columns show the statistics reported by EC-Diff as well as the prior technique (Bouajjani17, ). Specifically, Column 5 shows if EC-Diff detected the synchronization difference. Column 6 shows at which rank our analysis is conducted (Section 5): we iteratively increase the rank starting from 1, until a synchronization difference is detected. To be efficient, we bound the rank to 3 during our evaluation. Columns 7 and 8 show the number of differences in and . For a rank-1 analysis, it is the number of read-from edges; for a rank-2 or rank-3 analysis, it is the number of ordered sets of read-from edges. The next two columns show the total number of MayHb edges (used to compute MayRf) in and , respectively.

The last two columns compare the analysis time of our method and the model checking time of the prior technique (Bouajjani17, ) to check one different edge. For each benchmark, we limit the run time to one hour.

EC-Diff      Prior Technique (Bouajjani17, )
Name     LoC Threads       Type Difference Rank # of mayHB in # of mayHB in   Time (s) Time (s)
case1 52 3 Sync. yes 1 0 7 1,343 1,343 0.26 11.53
case2 53 3 Cond. yes 1 0 3 1,357 1,474 0.26 4.80
case3 67 3 Th.Order yes 1 2 0 546 482 0.19 46.64
case4 94 3 Sync. yes 2 0 1 421 421 0.20 8.59
i2c-hid (Bouajjani17, ) 76 3 Sync. yes 1 1 0 2,570 2,570 0.28 27.28
i2c-hid-noa (Bouajjani17, ) 70 3 Sync. yes 1 1 0 1,573 1,573 0.26 7.48
r8169-1 (Bouajjani17, ) 65 3 Order yes 1 1 0 870 852 0.25 3.38
r8169-2 (Bouajjani17, ) 80 3 Order yes 1 1 0 873 839 0.25 2.17
r8169-3 (Bouajjani17, ) 105 4 Order yes 1 1 0 769 769 0.25 8.37
rtl8169-1 (Bouajjani17, ) 578 8 Order yes 1 1 0 60,741 60,691 0.89 1580.16
rtl8169-2 (Bouajjani17, ) 578 8 Order yes 1 1 0 60,741 60,741 0.89 2384.14
rtl8169-3 (Bouajjani17, ) 578 8 Order no 3 0 0 60,741 60,741 2.40 0.00
cherokee (Yu09, ) 150 3 Sync. yes 1 0 2 1,148 1,148 0.31 7.59
transmission (Yu09, ) 91 3 Cond. yes 1 1 0 690 613 0.29 6.89
apache-21287 (Yu09, ) 74 3 Sync. yes 1 2 0 1,406 1,406 0.27 6.29
apache-25520 (Yu09, ) 181 3 Sync. yes 2 8 0 3,206 3,206 0.33 23.81
account (Beyer15, ) 82 4 Cond. yes 1 0 2 3,701 3,881 0.30 13.46
barrier (Beyer15, ) 138 4 Cond. yes 1 3 0 7,289 6,655 0.26 150.54
boop (Beyer15, ) 134 3 Sync. yes 1 3 0 2,625 2,625 0.25 8.90
fibbench (Beyer15, ) 63 3 Cond. yes 1 0 71 5,248 6,321 0.28 1483.33
lazy (Beyer15, ) 76 4 Cond. yes 2 0 6 3,409 3,549 0.24 32.16
reorder (Beyer15, ) 170 5 Cond. yes 1 3 0 9,493 8,737 0.40 12.79
threadRW (Beyer15, ) 147 5 Cond. yes 1 2 0 9,092 8,552 0.30 7.57
lineEq-2t (Bloem14, ) 90 3 Sync. yes 2 0 8 2,905 2,905 0.30 23.34
linux-iio (Bloem14, ) 114 3 Sync. yes 1 3 0 5,851 5,851 0.31 24.13
linux-tg3 (Bloem14, ) 130 3 Cond. yes 1 2 0 15,979 15,160 0.63 617.01
vectPrime (Bloem14, ) 127 3 Sync. yes 2 2 0 35,014 35,014 0.52 2.22
mozilla-61369 (Lu08, ) 84 3 Cond. yes 1 0 1 473 565 0.25 3.57
mysql-3596 (Lu08, ) 92 3 Cond. yes 1 1 0 773 733 0.25 3.82
mysql-644 (Lu08, ) 110 3 Cond. yes 1 0 2 1,343 1,434 0.33 5.40
counter-seq (Herlihy08, ) 47 3 Sync. yes 2 0 2 1,135 1,135 0.26 18.13
ms-queue (Herlihy08, ) 116 3 Sync. yes 2 2 0 5,754 5,754 0.59 29.01
mysql5 (Khoshnood15, ) 59 3 Sync. yes 2 0 4 1,283 1,283 0.20 22.92
freebsd-a (Yin11, ) 176 4 Cond. yes 1 0 22 7,910 10,109 0.33 25.40
llvm-8441 (llvm8441, ) 127 3 Cond. yes 1 0 10 3,042 3,118 0.41 16.36
gcc-25530 (gcc25530, ) 87 3 Sync. yes 2 2 0 806 806 0.20 12.15
gcc-3584 (gcc3584, ) 83 3 Sync. yes 2 2 0 1,843 1,843 0.24 17.23
gcc-21334 (gcc21334, ) 136 3 Sync. yes 2 8 0 5,290 5,290 0.35 195.20
gcc-40518 (gcc40518, ) 102 3 Sync. yes 1 0 8 3,027 3,027 0.25 14.31
glib-512624 (glib512624, ) 95 3 Sync. yes 1 198 0 5,748 5,748 0.32 3600.00
jetty-1187 (jetty1187, ) 69 3 Sync. yes 2 0 2 885 885 0.22 19.34
Total 251 151 338,913 339,849 15.57 3h
  • means verification of the edge in succeeded, but verification of the edge in timed out after an hour.

Table 1. Experimental results on the first set of benchmark programs.

Our results show EC-Diff often finishes each benchmark in a second whereas the prior technique can take up to 2,384 seconds (rtl8169-2). In total, EC-Diff took less than 16 seconds whereas the prior technique took more than 3 hours. In terms of accuracy, except for one program, EC-Diff detected all the synchronization differences. This has been confirmed through manual inspection where the reported differences are compared with the ground truth. Since we have randomly labeled the original and changed programs as and , some of the differences are in whereas the others are reported in . In total, EC-Diff found 251 differences in and 151 differences in .

The missed difference resides in rtl8169-3: after running the rank-3 analysis, our method still could not find it. The reason is because the differentiating behavior involves a deadlock and the patch that removed it. We explain why our method cannot detect it in Section 6.4.

6.3. Results on the Second Set of Benchmarks

Table 2 shows our results on the second set of benchmarks, consisting of six medium-sized programs. Note that these programs are already out of the reach of the prior technique (Bouajjani17, ) due to its requirement of manual code instrumentation; therefore, we only report the statistics of applying EC-Diff. Again, the original and modified programs are randomly labeled as and , respectively, to facilitate evaluation.

In total EC-Diff found 30 differences in and 42 differences in . Furthermore, all of them were found during rank-1 analysis, and confirmed by manual inspection. What is impressive is that these differences were identified by sifting through a combined total of 24 million MayHb edges, and yet, the analysis of all programs took only 140 seconds. The efficiency is, in large part, due to the restriction of our analysis on instructions that access global variables as opposed to all instructions in the program (refer to the last paragraph of Section 5.1). Otherwise, the number of MayHb edges would have been orders-of-magnitude larger.

EC-Diff
Name       LoC     Threads           Type Difference   Rank           # of mayHB in # of mayHB in     Time (s)
pbzip-1 (Yu09, ; Yang08, ) 1,143 5 Th.Order yes 1 6 0 782,846 773,934 14.98
pbzip-2 (Yu09, ; Yang08, ) 1,143 7 Th.Order yes 1 12 0 1,150,404 1,135,428 30.61
aget-1 (Yu09, ; Yang08, ) 1,523 4 Cond. yes 1 4 0 1,099,047 1,078,695 9.41
aget-2 (Yu09, ; Yang08, ) 1,523 6 Cond. yes 1 8 0 3,218,034 3,162,684 28.60
pfscan-1 (Yang08, ) 1,327 3 Cond. yes 1 0 6 2,094,446 2,107,760 19.72
pfscan-2 (Yang08, ) 1,327 5 Cond. yes 1 0 36 4,138,361 4,164,989 39.96
Total 30 42 12,483,138 1,242,3490 140.28
Table 2. Experimental results on the second set of benchmark programs.

6.4. Discussion

Now, we answer the two research questions.

Q1: Is EC-Diff accurate enough for identifying synchronization differences? The answer is yes. As shown in our experimental results, EC-Diff produced a large number of differences, the majority of which are at rank 1, which means they are individual read-from edges allowed in only one of the two programs, while the rest are at rank 2. Although we do not guarantee that EC-Diff finds all differentiating behaviors, these detected ones have been confirmed by manual inspection.

Given that these benchmarks contain real concurrency bug patterns reported and analyzed by many existing tools for testing and repair, the result of EC-Diff is sufficiently accurate. The success in a large part is due to the nature of these programs, where two versions behave almost same except for the thread synchronization. In such cases, our approximate analysis can come really close to the ground truth.

Q2: Is EC-Diff more efficient than the prior technique based on model checking? The answer is yes. As shown in our results, EC-Diff was 10x to 1000x faster and, in total, completed differential analysis of 13,500 lines of multithreaded C code in about 160 seconds. In contrast, the prior technique took a longer time to analyze a program.

Thus, we conclude that EC-Diff is effective in identifying synchronization differences in evolving programs. In practice, when developers update a program to fix concurrency bugs or remove performance bugs (e.g., by eliminating redundant locks), the differences in behavior are often reflected in (sets of) data-flow edges being feasible in one version but not in the other version. Thus, computing these (sets of) data-flow edges can be a fast way of checking if the changes introduce unexpected behaviors.

thread1() { lock(a); lock(b); ... unlock(b); unlock(a); }                         thread1() { lock(b); lock(a); ... unlock(a); unlock(b); }
Figure 11. Code from rtl8169-3: the original (left) and changed (right) versions.

The Missing Case: Although EC-Diff successfully detected most of the actual differences, it missed the one in rtl8169-3. Fig. 11 shows the code snippet of thread1 from the original program () on the left-hand side and the changed program () on the right-hand side. The purpose of this patch is to resolve a deadlock issue by changing the acquisition order of locks. Since EC-Diff focuses solely on data-flow edges, it is not able to detect behavioral differences related to locking only. In some sense, this is a limitation shared by techniques relying on the notion of abstract traces (ShashaS88, ; Bouajjani17, ): the two programs do not have data-related semantic difference other than the fact that a deadlock exists in one program but does not exist in the other program.

7. Related Work

There has been prior work on statically computing the semantic differences of sequential and concurrent programs.

For sequential programs, Jackson and Ladd (Jackson94, ) proposed a method for computing the semantic differences by summarizing and comparing the dependencies between input and output. Godline and Strichman (Godlin10, ) proposed the use of inference rules to prove the equivalence of two programs. In the SymDiff project, Lahiri et al. (Lahiri12, ; Lahiri13, ) developed a language-agnostic assertion checking tool for computing the differences of imperative programs. In the context of incremental symbolic execution (Person08, ), various change-impact analysis techniques were used to identify instructions that are affected by code modification and use the information to compute the corresponding test inputs (Marinescu13, ). However, these methods are not directly applicable to concurrent programs.

For concurrent programs, Joshi et al. (Joshi12, ) proposed the use of failure frequencies of assertions to compare two programs, while the general framework of refinement checking (Abadi91, ) could also be applied to traces of two programs. However, these techniques are limited to individual executions. Change-impact analysis (Lehnert11, ) were also applied to concurrent programs, e.g., in regression testing (YuRothermel14, ), prioritized scheduling (Jagannath11, ), and incremental symbolic execution (Guo16, ). However, these techniques focus on reducing the cost of testing and analysis as opposed to identifying the synchronization differences.

As we have mentioned earlier, the most closely related work is that of Bouajjani et al. (Bouajjani17, ), which computes the differences between partial data-flow dependencies of two concurrent programs using a bounded model checker. However, the method is costly; furthermore, it requires code instrumentation to insert assertions so they can be verified using a model checker. For example, it took about 30 minutes for a program (rtl8169) that can be analyzed by our method in less than a second.

Our method relies on the Datalog-based declarative program analysis framework, which previously has been applied to both sequential and concurrent programs as well as web applications (Heintze01, ; Hajiyev06, ; Horwitz95, ; Bravenboer09, ; FarzanK12, ; Guo16, ; Guo15, ; Lam05, ; Livshits05, ; Naik06, ; Whaley04, ; Sung16, ; ZhangMGNY14, ; AlbarghouthiKNS17, ). In the context of static analysis of concurrent programs, for example, Kusano and Wang (KusanoW16, ; KusanoW17, ) used Datalog in a thread-modular abstract interpretation to check the feasibility of inter-thread data-flow edges on sequentially consistent and weaker memory models. Sung et al. (Sung17, ) used a similar technique for modeling preemption scheduling of interrupts and thus improving the accuracy of static analysis for interrupt-driven programs. However, none of these existing methods computes the synchronization differences of evolving programs.

8. Conclusions

We have presented a fast and approximate static analysis method for computing the synchronization differences of two concurrent programs. The method uses Datalog to capture structural information of the programs, and uses a set of inference rules to codify the analysis algorithm. The analysis result, computed by an off-the-shelf Datalog solver, consists of sets of data-flow edges that are allowed by only one of the two programs. We implemented the proposed method and evaluated it on a large number of benchmark programs. Our results show the method is orders-of-magnitudes faster than the prior technique while being sufficiently accurate in identifying the actual differences.

Acknowledgments

This work was supported in part by the U.S. National Science Foundation (NSF) under grant CCF-1722710, the Office of Naval Research (ONR) under grant N00014-17-1-2896, the European Research Council (ERC) under the European Union’s Horizon 2020 research and innovation program (grant agreement No 678177).

References

  • [1] Gcc bug 21334. http://gcc.gnu.org/bugzilla/show_bug.cgi?id=21334.
  • [2] Gcc bug 24430. http://gcc.gnu.org/bugzilla/show_bug.cgi?id=25330.
  • [3] Gcc bug 3584. http://gcc.gnu.org/bugzilla/show_bug.cgi?id=3584.
  • [4] Gcc bug 40518. http://gcc.gnu.org/bugzilla/show_bug.cgi?id=40518.
  • [5] Glib bug 51264. https://bugzilla.gnome.org/show_bug.cgi?id=512624.
  • [6] Jetty bug 1187. https://jira.codejaus.org/browse/JETTY-1187.
  • [7] Llvm bug 8441. http://llvm.org/bugs/show_bug.cgi?id=8441.
  • [8] Martín Abadi and Leslie Lamport. The existence of refinement mappings. Theoretical Computer Science, 82(2):253–284, May 1991.
  • [9] Vikram Adve, Chris Lattner, Michael Brukman, Anand Shukla, and Brian Gaeke. LLVA: A Low-level Virtual Instruction Set Architecture. In ACM/IEEE international symposium on Microarchitecture, Dec 2003.
  • [10] Aws Albarghouthi, Paraschos Koutris, Mayur Naik, and Calvin Smith. Constraint-based synthesis of datalog programs. In International Conference on Principles and Practice of Constraint Programming, pages 689–706, 2017.
  • [11] Dirk Beyer. Software verification and verifiable witnesses. In International Conference on Tools and Algorithms for Construction and Analysis of Systems, pages 401–416, 2015.
  • [12] Sandeep Bindal, Sorav Bansal, and Akash Lal. Variable and thread bounding for systematic testing of multithreaded programs. In International Symposium on Software Testing and Analysis, pages 145–155, 2013.
  • [13] Roderick Bloem, Georg Hofferek, Bettina Könighofer, Robert Könighofer, Simon Außerlechner, and Raphael Spörk. Synthesis of synchronization using uninterpreted functions. In International Conference on Formal Methods in Computer-Aided Design, pages 11:35–11:42, 2014.
  • [14] Ahmed Bouajjani, Constantin Enea, and Shuvendu K. Lahiri. Abstract Semantic Diffing of Evolving Concurrent Programs, pages 46–65. Springer International Publishing, Cham, 2017.
  • [15] Martin Bravenboer and Yannis Smaragdakis. Strictly declarative specification of sophisticated points-to analyses. In ACM SIGPLAN Conference on Object Oriented Programming, Systems, Languages, and Applications, pages 243–262, 2009.
  • [16] Steven Dawson, C. R. Ramakrishnan, and David S. Warren. Practical program analysis using general purpose logic programming systems—a case study. In ACM SIGPLAN Conference on Programming Language Design and Implementation, pages 117–126, 1996.
  • [17] Azadeh Farzan and Zachary Kincaid. Verification of parameterized concurrent programs by modular reasoning about data and control. In ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages, pages 297–308, 2012.
  • [18] Benny Godlin and Ofer Strichman. Time for verification. chapter Inference Rules for Proving the Equivalence of Recursive Procedures, pages 167–184. Springer-Verlag, Berlin, Heidelberg, 2010.
  • [19] Shengjian Guo, Markus Kusano, and Chao Wang. Conc-iSE: Incremental symbolic execution of concurrent software. In IEEE/ACM International Conference On Automated Software Engineering, pages 531–542, 2016.
  • [20] Shengjian Guo, Markus Kusano, Chao Wang, Zijiang Yang, and Aarti Gupta. Assertion guided symbolic execution of multithreaded programs. In