MASARYK UNIVERSITY F A C U L T Y O F I N F O R M A T I C S Slicing of Parallel Programs MASTER'S THESIS Lukáš Tomovič Brno, Spring 2019 Declaration Hereby I declare that this paper is m y original authorial work, which I have worked out o n m y o w n . A l l sources, references, a n d literature used or excerpted during elaboration of this work are properly cited and listed i n complete reference to the due source. Lukáš Tomovič Advisor: doc. R N D r . Jan Strejček, P h . D . i Acknowledgements I w o u l d first like to thank to m y advisor doc. R N D r . Jan Strejček, Ph.D. for his valuable guidance a n d willingness. I w o u l d also like to thank to m y consultant R N D r . Marek Chalupa, without whose advice i n programming area this work w o u l d not exist. I also thank to Mgr. David Klaška for creating an algorithm for non-termination sensitive control dependency a n d therefore helped me solve one problem. Finally, I must express m y very p r o f o u n d gratitude to m y family a n d to m y fiancee for all their support a n d k i n d words. Thank you. iii Abstract This thesis is focused on static slicing of multi-threaded programs. We provide a reader w i t h an explanation of static program slicing i n general and after that we present problems connected w i t h slicing of parallel programs. Work discusses nowadays used methods for solving these problems and implements them into already existing D G slicer m a k i n g it capable of h a n d l i n g concurrent programs. Implemented features are: extension of points-to and reaching definition analyses for h a n d l i n g pthread A P I , computation of interference dependence edges and non-termination sensitive control dependency algorithm. A t the end we tested our implementation and evaluated benefit of usage of the extended slicer i n verification tools N i d h u g g and Divine. iv Keywords static analysis, static slicing, slicing, multi-threaded programs, program dependence graph, system dependence graph, interference dependence, control dependence, L L V M v Contents 1 Introduction 1 2 Static slicing of single-threaded programs 3 2.1 Program representation 4 2.2 System dependence graph building 8 2.2.1 Points-to analysis 8 2.2.2 Reaching definitions analysis 9 2.2.3 Control dependency analysis 10 2.3 Slicing algorithms 11 3 Non-termination Sensitive Control Dependency 13 4 S l i c i n g of multi-threaded programs 19 4.1 Interference dependence edges 19 4.2 Slicing algorithms 21 4.3 Critical sections 21 4.3.1 Locks 22 4.3.2 M u t u a l exclusion algorithms 24 5 Implementation 27 5.1 LLVM 27 5.2 POSIX Threads 29 5.3 D G 30 5.3.1 L L V M D e p e n d e n c e G r a p h 30 5.4 Points-to analysis 31 5.5 Reaching definition analysis 34 5.6 Interference dependence edges 35 5.7 Non-termination sensitive control dependency 37 5.8 Critical sections 41 6 Evaluation 43 7 C o n c l u s i o n 49 Bibliography 51 vii A Incorrect algorithm for N T S C D B Electronic attachments 1 Introduction Write software of high quality is hard. There exist many approaches and tools to help a programmer minimize the number of bugs. Static analysis is one of these approaches. This method analyzes source code of a given program without actually executing the program itself. A f terwards, it is able to claim something about behaviour of the program. However, static analysis can get only a limited amount of information, especially, it does not have run-time information. Therefore, w h e n static analysis is missing some specific knowledge, which is needed to decide whether there is a b u g i n the software or not, it usually makes a conservative assumption and concludes that there is the bug. A s a consequence, static analysis marks parts of programs where might potentially be a bug. Therefore, parts that are bug-free can also be marked. This approach is called over-approximating static analysis. Program slicing [1] is a technique used for computation of the set of program statements that can i n some sense affect specified points in the program (slicing criteria). The set computed i n this way is called a slice. It represents reduced source code, w h i c h preserves behaviour of the original program at slicing criterion points. The whole idea of this technique is to minimize the size of a program so other tools for verification and bug-finding could process smaller programs, what can make a finding of bugs more efficient and i n some cases it can even help find bugs, that w o u l d remain h i d d e n otherwise (without slicing). For example, there may be a bug-finding tool, that cannot cope w i t h some specific function, resulting i n the failing of the tool. Slicing could slice away this function and thus enabling the tool to do its work and potentially find a bug. This thesis is focused on static slicing [2], but there also exist other forms, e.g., dynamic slicing [3]. Symbiotic [4] is a bug-finding and verification tool, w h i c h uses several techniques as instrumentation, slicing, and symbolic execution to analyze programs. For slicing, this tool uses D G library [5], w h i c h contains slicer itself. The D G slicer was initially developed for slicing single-threaded sequential programs. The purpose of this thesis is to extend the functionality of the slicer to be capable of slicing multi- 1 i . INTRODUCTION threaded programs and to be ready for deployment i n bug-finding or verification tools capable of handling parallel programs. Chapter 2 explains program slicing for sequential programs i n a theory. It provides basic definitions a n d describes two algorithms. The chapter also gives a reader an insight into the capabilities of the original D G slicer (unchanged b y this work). Chapter 3 clarifies the term non-termination sensitive control dependency, explains what it solves, a n d provides an algorithm for acquiring it. A l l kinds of problems related to multi-threaded program slicing are covered i n Chapter 4: it describes the extension of the D G slicer by this work from a theoretical point of view. Chapter 4 discusses the implementation details for all implemented extensions and algorithms. Evaluation of the impact of deployment newly extended slicer i n the bug-finding tool N i d h u g g [6] a n d the verification tool Divine [7] is presented i n Chapter 6. Chapter 7 contains the conclusion of the w o r k a n d brief description of what can be done i n the future to improve the D G slicer further. 2 2 Static slicing of single-threaded programs First, we need to define a control flow graph, w h i c h is used in multiple definitions throughout the text. Definition 2.1 (Control Flow Graph, CFG). Control flow graph (CFG) of a program (or a procedure) is a quadruple Q = (N,E,nS/ne), where N is a finite set of nodes, E C N x N is a set of edges, ns E N is a unique enfry node of the program (or the procedure), and ne E N is a unique exzi node of the program (or the procedure). Every statement of the program (or the procedure) is represented b y a node i n Q. Let na and n& be nodes that represent two statements of the program (or the procedure). It holds that (na, n&) E E iff the execution of na can be immediately followed b y the execution of nj, i n the program (or the procedure). Unique exit node does not have to represent any program statement, i.e., it can be a n artificial node. Definition 2.2 (Path in a CFG). Let Q = (N, E, ns, ne) be a control flow graph. A path in Q is a finite or infinite sequence of nodes n\, rii,n^,... of N such that for every pair of successive nodes n,, n z + i it holds that (n,-, G E. Pflfh is called maximal iff it is infinite or the last node njt i n the path has n o successor, i.e., there is n o n E N such that (nk,n) E E. Static slicing is slicing i n the form of a static analysis. In further text the term "static slicing" w i l l be abbreviated to just "slicing". There can be one or multiple slicing criteria i n a source code of a program. Slicing criterion is essentially a statement i n the program. Definition 2.3 (Slice). A slice of a program w i t h respect to program point p and variable x consists of a set of statements of the program that might affect the value of x at p [8]. Definition 2.3 uses pair (p,x) where p is a program point and x a variable i n p, as a slicing criterion. In our setup slicing criterion is represented by a program statement. Program statement precisely determines a point in a program, and we require all program statements that might affect any variable used b y slicing criterion to be i n the slice. We also require any statement that w o u l d change the reachability 3 2. STATIC SLICING O F SINGLE-THREADED P R O G R A M S of the slicing criterion i n the program to be i n the slice. If the slice misses any of such statements, w e call it a n incorrect slice. Otherwise it is called a correct slice. If slice does not contain any other statements, it is called minimal. If there are multiple slicing criteria, a correct slice is a u n i o n of the correct slices resulting from individual slicing criteria. If a slice is correct and m i n i m a l , it is called precise. Weiser [1] s h o w n that the computation of precise slices is undecidable because of the evaluation of conditional branches. Chapter 5 discusses implementation details, but we should at least mention what constraints we put o n programs that can be sliced the way this thesis suggests. O n e constraint is that exceptions are not supported. Another limitation is that the program cannot contain an infinite loop preceding slicing criterion in a C F G . Chapter 3 discusses this matter in more detail. Besides that everything that can be written in languages like C is supported. 2.1 Program representation There are several algorithms for slicing. M a n y of them r u n o n the specific representation of a program, called system dependence graph [9]. This approach is also used i n D G . Three steps have to be done [5] to slice the source code of a program this way: 1. Create a system dependence graph from the source code. 2. Execute the actual slicing algorithm o n this graph. 3. Transform the sliced graph back into the source code. A program m a k i n g these three steps is called a sheer. This thesis focuses o n the first and second step. Transformation of a graph back to source code is implemented inside D G and this implementation required n o modification to extend the D G slicer to work o n multithreaded programs. Before diving into details of system dependence graph representation, its basic building block called program dependence graph [10] is described. Section 2.3 explains w h y program dependence graph is not sufficient for our purposes and system dependence graph is preferred. 4 2. STATIC SLICING OF SINGLE-THREADED P R O G R A M S Program dependence graph (PDG) is a graph, where each p r o g r a m statement is represented as a single node. Between these nodes are two types of edges: control dependence edges and data dependence edges. Figure 2.1 shows an example of P D G . cd Intuitively, node n; is control dependent on node n,, written n,- —> n;, if the execution of node n,- can affect whether node n; w i l l be executed or not. There exist several definitions of control dependencies [11]. Section 2.2.3 provides the one used i n D G before this work and it also explains w h y there is a need for another definition, that is provided in Chapter 3. Data dependency is simpler, because there is only one definition relevant i n the context of this thesis. Definition 2.4 (Data dependency). Node n; is data dependent on node n{, written n, —>• m, if there is a path i n control flow graph of a given program leading from node n,- to n; and such that n,- writes into a variable, n; reads that variable, and there is no other node on the path from n,- to n, that writes to that variable. 1 2 3 4 5 6 7 e l s e i n t i i f ( i l l 7. 0 2 l l + i ; == 0) / 2 ; J = i * i j = i * i p r i n t ( j ) ; p r i n t ( j ) control dependence edges data dependence edges Figure 2.1: P D G and program w h i c h it represents 5 2. STATIC SLICING O F SINGLE-THREADED P R O G R A M S A system dependence graph (SDG) is a graph that contains multiple P D G s (for each procedure one), w h i c h are connected at call-sites. A call-site [9] is represented b y a call node c, w h i c h is connected to the entry node e of the called procedure via a call edge. For each parameter p, w h i c h is passed into the function, there are two nodes, a n actualin node a,- and a formal-in node connected through a parameter-in edge a,- —V /(. Additionally, for each modified parameter and returned value, there is an actual-out node a0 and a formal-out node f0 con- po nected through a parameter-out edge f0 —> a0. Summary edges represent propagation of returned value and changes i n modified parameters from callee to caller. There is a summary edge from an actual in-node to actual out-node if the value of in-node can affect the value of outnode. In a system dependence graph, a program dependence graph of a function has an entry node which does not represent any program statement, i.e., it is a n artificial node. Figure 2.2 shows a n S D G w i t h calls of procedures. For now, the presence of some control dependence edges i n this figure is unclear. This is clarified further i n Section 2.2.3. For understanding system dependence graph it is enough to accept the fact those edges are there. Since every program statement is represented by a node i n an S D G , slicing criterion is a corresponding node. There are several algorithms for static slicing o n SDGs. One of them uses a backward reachability technique. Figure 2.2 represents a result of such an algorithm. A l l slicing criteria are marked to be a part of a slice; i n this case, it is node p r i n t (b). Then the algorithm traverses edges backward and all visited nodes (all backward reachable nodes from slicing criteria) are marked to be i n the slice. 6 2. STATIC SLICING OF S I N G L E - T H R E A D E D P R O G R A M S ( b = 1 ) [b = bar(b7] control dependence edges data dependence edges parameter edges call edges summary edges 1 i n t f o o ( i n t x) { 2 i f ( x % 2 == 0) 3 x = x / 2; 4 ) e l s e ( 5 x = x — 1; 6 ) 7 r e t u r n x ; 10 11 12 13 14 15 16 i n t b a r ( i n t y) { i f ( y > 0) ( y = y / 2; ) e l s e ( y = y * y ; 1 r e t u r n x ; 17 18 19 20 21 22 23 i n t main () { i n t a = 1; i n t b = 1; a = foo ( a ) ; b = b a r ( b ) ; Figure 2.2: Example of an S D G ; highlighted nodes are part of the slice w i t h respect to p r i n t (b) 7 2. STATIC SLICING OF SINGLE-THREADED P R O G R A M S 2.2 System dependence graph building This section describes what it takes to create the system dependence graph of a program. 2.2.1 Points-to analysis If a program to be sliced contains pointers, things are a little complicated. Some parts of program can write to the memory through a pointer. Example: 1 x = 5 ; 2 p = & x ; 3 q = p ; 4 * q = 7; 5 p r i n t ( x ) ; At this point, to correctly build the S D G w i t h all its edges, it is needed to k n o w what object i n m e m o r y the pointer q is pointing to, so the writing to memory through the pointer can be interpreted correctly as writing to the corresponding object, i n this case x. There should be data dependence edge from * q = 7 to p r i n t (x). K n o w i n g to what objects pointers are pointing to is called points-to information. It is important to notice that it cannot be simply obtained just f r o m the syntax. In this example, to know the object pointer q is pointing to, information what object pointer p is pointing to is required. Points-to information is essential not only for computing of all required data dependence edges i n the S D G , but also for correct manifestation of call-sites i n the S D G . This is because program can contain calls of function v i a a function pointer: the pointer pointing to some function: 1 a = 1; 2 b = 2 ; 3 f o o ( a , b) ; 4 p = & f o o ; 5 p ( a , b ) ; In this case the S D G has to contain a call node that is connected to entry node of the right function. It is necessary to know that pointer p points to f oo function, so call of p can be interpreted correctly. 8 2. STATIC SLICING OF SINGLE-THREADED P R O G R A M S Points-to information can be obtained by a points-to analysis. There are several algorithms for this. The algorithms differ i n both precision and efficiency. One important trait of these algorithms is flowsensitivity. Consider the following piece of code: 1 X = r a n d o m _ 2 y = r a n d o m _ 3 p = & x ; 4 *P = * p + 1; 5 p = & y ; 6 *P = * p + 1; 7 i f (x == 7) 8 p = & x ; 9 e l se 10 p = & y ; 11 * p = * p + l At line 4, p is pointing to x variable (set up this way at line 3). A t line 5 p is no more pointing to x, but now it is pointing to y. Object pointer p is pointing to is changing during the execution of the program. Flow-sensitive analysis takes into account the order of statements in a program, i.e., it computes a separate points-to set for each pointer and a program location, to which the pointer can point at that program location. In the example above for pointer p at line 4 the points-to set w o u l d be {x} and at line 6 {y}. Points-to set of pointer p at line 11 would be {x, y}, because static analysis does not know how conditional branches are going to be evaluated. Flow-insensitive analysis ignores the order of program statements. For a pointer there is only one points-to set for all program locations. Resulting points-to set contains all objects a pointer can point to at any program location. In our example, points-to set for pointer p w o u l d be {x, y}. D G contains both flow-sensitive and flow-insensitive points-to analysis algorithms. 2.2.2 Reaching definitions analysis Reaching definition analysis is another required analysis for computing data dependencies. 9 2. STATIC SLICING O F SINGLE-THREADED P R O G R A M S Definition 2.5 (Reaching definition). Let Q = (N, E,ns,ne) be a control flow graph of a given program. A reaching definition for a node n G N is every node m G N such that m defines some variable v which n reads a n d there is a path i n Q from m to n such that no other node on this path redefines v. The purpose of the analysis is to compute the set of reaching definitions for every node i n C F G of a given program. D G uses reaching definition analysis for computing data dependence edges. Standard approach for computing reaching definitions is a classical data-flow analysis [12]. Here, it is important to note that reaching definition analysis requires points-to information, so points-to analysis has to be finished before the reaching definition analysis starts. 2.2.3 Control dependency analysis Control dependency has been described very intuitively i n section 2.1. Before we provide a precise definition of control dependency used i n D G , post-dominance relation has to be defined first. Definition 2.6 (Post-dominance). Let Q = ( N , E, ns, ne) be a control flow graph. Node n G N is post-dominated by node m G N iff all paths in Q from n to ne go through m. Definition 2.7 (Control dependency). Let Q = ( N , E, ns, ne) be a control flow graph. N o d e m G N is control dependent o n node n G N if there is a path i n Q from n to m and every node o n that path except n is post-dominated by m. Definition 2.7 is used for dependencies inside a procedure. For slicing of programs w i t h procedure calls there are some additional dependencies. For each call of a procedure all actual in-nodes a n d out-nodes are control dependent o n the call node. For example i n Figure 2.2, X[n = a and a = xout are actual parameter nodes (common name for in-nodes a n d out-nodes), w h i c h are control dependent o n call node a = f oo (a). Likewise all formal in-nodes and out-nodes are control dependent o n entry node of the called procedure. In this way, system dependence graph contains all required control dependencies for slicing programs with procedure calls. This is called interprocedural 10 2. STATIC SLICING OF SINGLE-THREADED P R O G R A M S slicing. It is slicing of programs, where slice can potentially go across boundaries of procedure calls. If there is such a node that it is not control dependent o n any other node i n a control flow graph of a procedure, it w i l l be control dependent on an entry node of the procedure i n a system dependence graph. D G contains algorithm [13] for computation of control dependencies according to Definition 2.7 and adds control dependencies for each call of a procedure as described above. 2.3 Slicing algorithms Simple backward reachability algorithm was already presented i n Section 2.1. This section describes another algorithm and clarifies the term context-sensitivity [14]. Before that, it is needed to clear u p the notion of precise algorithm. If a slicing algorithm outputs precise slices for each program and slicing criterion, then it is called precise. Consider Figure 2.3a. Slicing criterion is p r i n t (b). The simple backward reachability algorithm computes the slice that contains also nodes a = 1, a = foo(a), a n d x ! W = a, that are i n fact not needed. The main reason for this is that the backward reachability algorithm cannot distinguish calling context of the function f oo. Once some node of a procedure is part of a slice, all call of the procedure i n the program are in the slice, i n this case a = f o o ( a ) a n d b = foo(b). Thus, backward reachability algorithm is context-insensitive. Another slicing algorithm is called two-pass graph traversal [14] or two-phase algorithm. It fully uses the potential of the S D G structure. In the first phase, edges are traversed backward, starting i n the slicing criterion. If the algorithm encounters parameter-out edge (in Figure 2.3a the edge f r o m b = xout to xout = x), it puts the node (xout = x) into the list L instead of traversing edges f r o m it. Second phase begins i n nodes that are i n list L. It traverses all edges backward, except parameter-in edges and call edges. Thanks to that the algorithm does not have to return from callee to caller, w h i c h makes it capable of not including all calls of the particular procedure (in this case procedure foo), hence it is context-sensitive. This is possible due to summary edges, which take care of propagating return values and modified arguments 11 2 . STATIC SLICING O F S I N G L E - T H R E A D E D P R O G R A M S control dependence edges data dependence edges parameter edges call edges summary edges (a) Slice with respect to print(b). Green nodes represents first phase of two-phase algorithm, grey nodes second phase, and all highlighted nodes backward reachability algorithm. 1 i n t f o o ( i n t x) { 2 i f ( x % 2 == 0) { 9 i n t m a i n ( ) { 3 x = x / 2; 10 i n t a = 1; 4 ) e l s e ( 11 i n t b = 1; 5 x = x - 1; 12 6 ) 13 a = f o o ( a ) ; 7 r e t u r n x ; 14 b = f o o ( b ) ; 8 1 15 1 from callee to caller. That is w h y an S D G is preferred to a P D G . Program dependence graph is not suitable for two-phase algorithm, because it does not have the structure to m o d e l call of procedures properly. P D G is defined only for intraproderural slicing (only one procedure that contains no calls). Note that the two-phase algorithm is contextsensitive even without calling context being explicitly modeled [14]. The algorithm does not remember call stack of functions explicitly. 12 3 Non-termination Sensitive Control DepenChapter 2 briefly mentioned the trouble w i t h infinite loops. N o w it is time to examine this more deeply. Consider Figure 3.1. W h e n a user runs this program, value of variable x is never going to be printed because of the infinite loop incrementing variable i . What happens if the program is sliced w i t h respect to p r i n t (x)? The entire loop is sliced away and p r i n t (x) can be executed. This means that the behaviour of the program has changed at the slicing criterion point. However, the desired effect is that if the original program could not get to the slicing criterion, neither should its slice. This is the m a i n drawback of Definition 2.7 and it is also the reason w h y there is a requirement for another control dependency definition. A slicer w o u l d produce the desired output if node p r i n t (x) was somehow dependent on the loop right before it. This leads to nontermination sensitive control dependency ( N T S C D ) [11]. A s the name suggests, this definition of control dependency is sensitive to nontermination structures in programs and guarantees that if the original program d i d not terminate, the sliced one w i l l not terminate either, provided the slicing criterion was after the non-terminating structure in the control flow graph (in Figure 3.1, p r i n t (x) is after the infinite loop i n the C F G ) . Definition 3.1 (Non-Termination Sensitive Control Dependency). N o d e rij is non-termination sensitive control dependent (ntscd) on node written n,- ntscd y « i f has at least two successors n^, n\ such that all maximal paths starting at go through n,- and there exists a maximal path starting at ft/ such that it does not go through n,-. Using Definition 3.1 instead of 2.7 causes among others p r i n t (x) to be control dependent on true. Note that variable i is never required (in the slice with respect to p r i n t (x)) and thus w i l l be sliced away. Node i = i + 1 w i l l be sliced away as well. The resulting slice w i l l however contain infinite loop w i t h empty body, preserving non-termination of this particular program. 13 3. N O N - T E R M I N A T I O N SENSITIVE C O N T R O L D E P E N D E N C Y [TTo] [true) ( i = i + T print(x) 1 i n t x = 0; 2 i n t i = 0; 3 w h i l e ( t r u e ) { 4 i = i + 1; 5 } 6 p r i n t ( x ) ; Figure 3.1: A control flow graph w i t h the corresponding source code Our main motivation behind usage of another control dependency definition was to solve some issues with critical sections i n concurrent programs. Section 4.3.2 covers this topic. However, N T S C D solves also problems i n sequential programs as w a s described above a n d it is a general notion inherently not specific just for multi-threaded programs. Ranganath et al. [11] provided algorithm for computation of nontermination sensitive control dependencies. However, it turns out this algorithm might not w o r k correctly i n specific cases. A p p e n d i x A contains a n example, where this algorithm fails. This issue was consulted w i t h a colleague David Klaska, w h o designed another algorithm and provided proof of correctness. This chapter describes both the algorithm and proof of correctness. Algorithm 1 shows pseudocode for computation of N T S C D . This algorithm runs o n a control flow graph. Procedure computeDependees computes a n d returns the set of nodes w h i c h one specified node (called initial node) is dependent on. Procedure computeDependencies calls procedure computeDependees for every node of the C F G . Notions successorsNumber(n) represents number of successors of node n a n d term predecessors(n) represents the set of predecessors of node n i n a C F G . Basic idea behind the algorithm is as follows. We want to compute set of nodes o n w h i c h a node n is dependent. Let us call this node the initial node. N o d e n,- is called red iff all m a x i m a l paths f r o m ft; go through the initial node ft. Otherwise ft, is black. The algorithm titscd computes the set of red nodes for n. Then w e set nx > n iff nx has at least one r e d successor a n d one black successor. Procedure 14 3. N O N - T E R M I N A T I O N SENSITIVE C O N T R O L D E P E N D E N C Y computeDependees computes red nodes for node n (the initial node). It proceeds i n two phases: 1. M a r k n as red and all other vertices as black. 2. A s long as there is such a black node v that all of its successors are red, mark v as red. Lemma 3.2. For every node v it holds that v is red iff v is the initial node or all of its successors are red. Proof. It directly follows from the two phases described above. • Implementation details are as follows. In the initial phase, the procedure sets the color of each node to black and the counter of each node to the outdegree of the node. After that it marks the initial node as red and starts backward reachability f r o m it. U p o n traversing an edge, it decreases a counter of a visited node by one. W h e n counter of a node drops to zero, the node is marked as red and only at this moment backward reachability continues from this node. Lemma 3.3. Every edge is traversed at most once per computeDepen- dees. Proof. For each node n, counter counter[n] is initialized to the outdegree of n at the initialization phase ( A l g o r i t h m 1, line 13). D u r i n g the graph traversal, counter[n] is only decremented (line 26), never incremented. Thus the value of counter[n] can drop from one to zero at most once per the graph traversal. O n l y at this point (line 27), w e visit all predecessors of node n by edges leading to n. Thus every edge is traversed at this point or never. • Lemma 3.4. For every node n, counter[n] never drops below zero. Proof. In the beginning, counter[n] is set to the outdegree of node n for every node n (line 13). D u r i n g the graph traversal, counter[n] is decremented only w h e n the node is visited via backward traversal of some outgoing edge. Every edge is traversed at most once (Lemma 3.3), thus counterln] can be decremented by at most number of successors of n, w h i c h is the initial value of counterln]. • 15 3. N O N - T E R M I N A T I O N SENSITIVE C O N T R O L D E P E N D E N C Y Algorithm 1 Non-Termination Sensitive Control Dependency procedure computeDependencies(G(N, E)) allDependees[\N\]; > declaration for each n i n N do allDependees[n] «— computeDependees(G(N,E),n); return allDependees; procedure computeDependees(G(N, E),node) color [ | N | ]; > declaration counter [ | N | ]; > declaration dependees «— 0 ; for each n i n N do color[n] «— Wad:; counter[n] «— successorsNumber(n); visitlnitialNode (color, counter, node); for each n i n N do if n has a red and a black successor then dependees «— dependees U {n}; return dependees; procedure visitInitialNode(color, counter, node) color[node] «— red; for each m i n predecessors (node) do visit (color, counter, m); procedure visit(color, counter, node) counter[node] «— counter[node] — 1; if counter[node] = 0 then color[node] «— red; for each m i n predecessors (node) do visit (color, counter, m); 16 3. N O N - T E R M I N A T I O N SENSITIVE C O N T R O L D E P E N D E N C Y Theorem 3.5. Let n be the initial node. For each v G N, it holds that v is already red at line 15 in thefunction computeDependees(G(N, E), n) iff all maximal paths starting at v go through n. Proof. => by contradiction: Assume that there is red node v and maximal path p starting at v such that it does not go through n. Every successor of any red node other than n is also red (Lemma 3.2), thus all nodes o n p are red. Since the graph is finite, p must contain a cycle c. N o w let x be the first node marked as red among nodes o n c and let y be its successor o n c. Node x could be marked as red only w h e n all of its successors (including y) were already red. This contradicts the fact that x was the first node o n c that became red. Figure 3.2 depicts this situation. (a) Will happen (b) Cannot happen Figure 3.2: Numbers are counters of particular nodes. Dotted edge means there is an oriented path. by contraposition: Let us take a black node v. W e construct a maximal path VQ, V\, ... of black nodes inductively as follows: VQ = V and Vi+\ is any black successor of V{. If V{ does not have a black successor, it has no successor at all (if it w o u l d have a non-black, i.e., red successor and no black successor, V{ w o u l d be red as well since all of its successors are red). Thus we have found a maximal path starting at v not leading through n (because n is red). • We assume that \N\ < \E\. L e m m a 3.3 says that every edge is traversed at most once per computeDependees. This gives us complexity 17 3. N O N - T E R M I N A T I O N SENSITIVE C O N T R O L D E P E N D E N C Y of computeDependees, which is 0 (| E \). Procedure computeDependencies calls computeDependees for each node, thus complexity of computeDependencies is O(|N'| x |E|). 18 4 Slicing of multi-threaded programs Multi-threaded programs present an entire different category of issues that are not present i n sequential programs. This chapter focuses on these problems and suggests solution for every single one of them. Before diving into complexity of these issues, it is necessary to provide definition for threaded control flow graph (tCFG), since definition of C F G from Chapter 2 is not correct for multi-threaded programs. Definition 4.1 (Threaded Control Flow Graph, tCFG). Threaded control flow graph (tCFG) of a p r o g r a m is a quadruple Q = ( N , E,ns,ne), where N is finite set of nodes, E C N x N is set of edges, ns E N is unique entry node of the program, and ne G N is unique exit node of the program. Every statement of the program is represented by a node i n Q. Let na and n& be nodes that represent two statements of the program. It holds that (na, n^) G E iff one of the following holds: 1. Execution of na can be immediately followed by the execution of njy inside a single thread of the program. 2. na is a node which spawns a new thread (fork node) and is the entry node of the procedure being spawned as the new thread. 3. na is exit node of procedure's thread and n& is join node i n another thread, w h i c h is joining thread of na's procedure. 4.1 Interference dependence edges One phenomenon that comes with multi-threaded programs is writing to a shared variable from multiple threads. Consider Figure 4.1. Node j — x + 1 is reading from a shared variable x. N o d e x — i * 2 + 1 is writing to the variable x and it can happen that the order of these instructions w i l l be reading after writing. In this case, it is required that if n o d e ; = x + 1 is a part of a slice, node x — i * 2 + 1 is also i n the slice. Thus, j — x + 1 should be dependent on x = i * 2 + 1. This is obviously not control dependence, because x — i * 2 + 1 cannot affect whether j = x + 1 executes or not. Intuition w o u l d be to consider this as a data dependence. The problem is that this k i n d of dependency 1 9 4. SLICING O F MULTI-THREADED P R O G R A M S join Figure 4.1: Threaded Control Flow G r a p h does not correspond to Definition 2.4, because there is no path i n the t C F G f r o m x = i * 2 + 1 to j = x + 1 as these instructions are o n different threads. For this purpose we need to use completely new type of dependency called interference dependency. To provide definition of this dependency, the term may-happen-in-parallel needs to be clarified. It is said that two statements A and B may-happen-in-parallel ( M H P ) if there are two possible executions of a given program such that i n one execution A happens before B and i n the other B happens before A. Definition 4.2 (Interference dependency). Let Q = (N,E,ns,ne) be a threaded control flow graph. N o d e rij G N is interference dependent on node n,- G N, written n,- rij, if ft; writes to a shared variable v, rij reads variable u and it holds that n,- and n,- may-happen-in-parallel. Without interference dependence edges, computed slices could be i n correct. A d d i t i o n of interference dependence edges into S D G results into n e w type of graph called concurrent SDG (cSDG) [15]. Spawning of threads requires no additional changes, it can be modeled as function call, i.e., there w i l l be fork-site, actual a n d formal in-nodes and out-nodes, a n d summary edges. Summary edges do not handle immediate changes i n shared variables, but this is of no concern. These immediate changes are handled by interference dependence edges. 20 4. SLICING O F MULTI-THREADED P R O G R A M S There exists analysis for obtaining M H P information, called mayhappen-in-parallel analysis ( M H P analysis) [16]. Another option w o u l d be to over-approximate this information and conservatively assume, that anything may-happen-in-parallel w i t h anything. That w o u l d result in potentially more interference dependence edges a n d computed slices would be less precise than w i t h proper M H P information, but it would preserve correctness of slices. This thesis uses the latter approach and is designed to be extended to use proper M H P analysis i n the future. 4.2 Slicing algorithms It is obvious that the backward reachability algorithm produces correct slices even if r u n n i n g o n c S D G instead of S D G . It traverses all edges backward, n o matter the type of edges. The two-phase algorithm is a more interesting case. Does the extension of S D G to c S D G preserves correctness of two-phase algorithm i n terms of outputting correct slices? A s it turns out, the algorithm is not correct anymore. Figure 4.2 shows a n example, where two-phase algorithm computes an incorrect slice. Let c = getout be the slicing criterion. The first phase visits green nodes. Second phase starts at node getout = x a n d visits yellow nodes. However, the second phase does not traverse call edges and parameter-in edges, so it does not return back to caller. This means that nodes set(a), bin = a, and a = 3 are not visited. Clearly, they should be i n the slice. Two-phase algorithm can be slightly modified [15] so it produces correct slices. Modified algorithm is called iterated two-phase algorithm. Modification is that u p o n encountering interference dependence edge, slicer marks the node reached through this edge as n e w slicing criterion a n d continues. Final slice is the u n i o n of all slices for all encountered slicing criteria. O u r w o r k uses backward reachability algorithm, but all implemented dependencies a n d graphs are designed to work w i t h iterated two-phase algorithm. 4.3 Critical sections A l t h o u g h the point of control dependence edges is to be i n S D G s for slicing, Definition 2.1 uses the notion of control flow graph and for 21 4. SLICING OF MULTI-THREADED P R O G R A M S a = 3 b = b{, E N T R Y thread_l [ set(a)' ENTRY set control dependence edges data dependence edges parameter edges call edges interference dependence edges E N T R Y thread_2 get() / E N T R Y get ut = x -> i n t x = 0; //global vat v o i d t h r e a d _ l () { i n t a = 3; s e t ( a ) ; v o i d s e t ( b ) { x = b ; v o i d t h r e a d _ 2 ( ) { i n t c = g e t ( ) ; i n t g e t ( ) | r e t u r n x ; Figure 4.2: Fragment of a c S D G showing two-phase algorithm. Green nodes represent the first phase, yellow the second phase, and grey nodes should be i n the slice, but they are not. better readability, some figures of C F G s i n this chapter contain control dependence edges to better illustrate discussed ideas. Concurrent programs often contain mechanisms to prevent race conditions. If a program to be sliced incorporates mechanism like that and some part of a slice is protected b y that mechanism, then it is essential that this mechanism is also i n the slice. Section 4.3.1 deals with locks and section 4.3.2 discusses mutual exclusion algorithms. Both are ways for race condition prevention. 4.3.1 Locks Locks are w i d e l y used i n parallel programs. Consider Figure 4.3. If node assert(x==2) is marked as a slicing criterion, there is no dependency that w o u l d force lock(l) and unlock(l) to be a part of a slice. C o m - 22 4. SLICING O F MULTI-THREADED P R O G R A M S x = 0 fork ENTRY foofork ENTRY foo lock(l) X = X + 1 unlock(l) oin l v , J assert(x == 2) 3 r lock(l) X = X + 1 unlock(l) EXIT foo control flow edges > control dependence edges • Figure 4.3: Threaded Control Flow G r a p h w i t h Locks - thick edges are added control dependence edges as suggests section 4.3 mand x = x + 1 does not have to be atomic (typically it is not unless some special atomic type w o u l d be used for the variable x). Resulting slice w o u l d contain no mechanism to prevent race condition and behaviour at slicing criterion w o u l d change. W h i l e i n the original program a condition i n the assert node w o u l d be always evaluated to true, there w o u l d be an execution of the sliced program such that the condition w o u l d be evaluated to false. We propose to solve this issue by making every node i n a critical section control dependent on the corresponding lock and the lock control dependent on the corresponding unlock (thick edges i n Figure 4.3). 23 4- SLICING OF MULTI-THREADED P R O G R A M S In this way, if any of the nodes i n a critical section are marked as a part of a slice, the corresponding lock and unlock w i l l also be marked to be i n the slice. This also corresponds to the intuitive view o n control dependency. Node A is control dependent on node B, if the execution of B can affect whether A w i l l be executed. In this sense, executing of lock can effect whether nodes i n a critical section w i l l be executed or a deadlock w i l l occur. If there is no deadlock i n the program, lock can at least affect w h e n nodes i n the critical section w i l l be executed. 4.3.2 Mutual exclusion algorithms Locking mechanism is not the only one to handle critical sections. Another solution for preventing race conditions are the algorithms for mutual exclusion. 1 b o o l w a n t s _ t o _ e n t e r [] = ( f a l s e , f a l s e ) ; 2 i n t t u r n = 0; 3 i n t x = 0; 4 Figure 4.4 shows code for Dekker's algorithm for mutual exclusion. The main idea of this algorithm is based on communication via shared variables. Imagine the p r o g r a m to be sliced w i t h respect to line 23 5 i n t m a i n ( ) ( 6 f o r k ( t h r e a d _ 0 ) ; 7 w a n t s _ t o _ e n t e r [ 1 ] = t r u e ; 8 w h i l e ( w a n t s _ t o _ e n t e r [0]) ( 9 i f ( t u r n != 1) ( 10 w a n t s _ t o _ e n t e r [1] = f a l s e ; 11 w h i l e ( t u r n != 1 ) ; 12 w a n t s _ t o _ e n t e r [1] = t r u e ; 13 ) 14 ) 15 // critical section 16 x = x + 1; 17 18 t u r n = 0; 19 w a n t s _ t o _ e n t e r [ 1 ] = f a l s e ; 20 21 // remainder section 22 j o i n ( t h r e a d _ 0 ) ; 23 a s s e r t ( x == 2) 24 r e t u r n 0; 25 ) 26 v o i d t h r e a d _ 0 ( ) ( 27 w a n t s _ t o _ e n t e r [0] = t r u e ; 28 w h i l e ( w a n t s _ t o _ e n t e r [ 1 ]) { 29 i f ( t u r n != 0) ( 30 w a n t s _ t o _ e n t e r [0] = f a l s e ; 31 w h i l e ( t u r n != 0 ) ; 32 w a n t s _ t o _ e n t e r [0] = t r u e ; 33 ) 34 ) 35 // critical section 36 x = x + 1; 37 38 t u r n = 1; 39 w a n t s _ t o _ e n t e r [0] = f a l s e 40 // remainder section 41 ) Figure 4.4: Dekker's algorithm 24 4. S L I C I N G OF MULTI-THREADED P R O G R A M S i n m a i n function (assert(x == 2)). If a slicer w o u l d use the original definition for control dependency (Definition 2.7) then there w o u l d be no dependencies forcing while loops (Dekker's algorithm) to be in a slice. A s a result, sliced program w o u l d contain no mechanism for mutual exclusion and thus it could behave differently at the point of slicing criterion. This was i n fact our original motivation behind using non-termination sensitive control dependency as described i n Chapter 3. N T S C D solves this problem. 25 5 Implementation 5.1 LLVM The LLVM project [17] is an open source set of modular compiler and toolchain technologies. It started as a research project at the University of Illinois and n o w is widely used i n both production and academic environment. One of the b i g advantages of L L V M infrastructure is the independence on programming language and target architecture. The entire project contains several subprojects, among others LLVM Core libraries (implemented i n C++), LLVM intermediate representation (LLVM IR) and Clang - C, C++, and Objective-C compiler. L L V M IR is a code representation used i n all phases of L L V M compilation strategy. Simplified view can be that it is an architecture independent assembly language that provides type safety. L L V M IR has three possible forms: in-memory compiler IR, on-disk bitcode representation, and h u m a n readable assembly language representation. These three forms are equivalent, so we discuss only h u m a n readable form. L L V M IR is structured into modules, w h i c h represents translation units — one module corresponds to a single source code file. A module contains global values — c o m m o n name for global variables and junctions. Functions consists offunction type and one or multiple basic blocks, which is a single unit of control flow. Basic block is a linear sequence of instructions w i t h no branching inside. It can have multiple successors (other basic blocks) and that is where branching of control flow happens. Figure 5.1 shows a m o d u l e w i t h two functions and one global variable. Global values have name prefixed w i t h "@" sign, all other variables' names are prefixed w i t h "%" sign. Type system of L L V M IR makes possible some optimizations directly on IR without further analyses. Since this system is really rich i n types and it is not so important i n context of this work, we describe only some of types to understand how the type system works. L L V M IR contains simple types and types that can be derived from them. Simple types are v o i d (same meaning as v o i d i n C language), integral types (e.g. i32 represents 32 bit integer; instead of 32 can be used w i t h bit w i d t h of arbitrary number f r o m 1 to 2 2 3 — 1, e.g i49 represents 49 bit integer),floating-point types (half, 27 5. I M P L E M E N T A T I O N 1 @x = g l o b a l 132 0, a l i g n 4 2 3 define 132 @ £ o o ( i 3 2 ) #0 I 4 %2 = a l l o c a , a l i g n 4 5 %3 - a l l o c a i32 , a l i g n 4 6 store i32 %0, i32* %3, a l i g n 4 7 %4 = load i32 , i32* %3, a l i g n 4 8 %5 = srem 132 %4, 2 9 %6 = icmp eq i32 %5, 0 10 br 11 %6, label %7, label %10 11 12 ;