Context-Updates Analysis and Refinement in Chisel

09/20/2017 ∙ by Irina Mariuca Asavoae, et al. ∙ 0

This paper presents the context-updates synthesis component of Chisel--a tool that synthesizes a program slicer directly from a given algebraic specification of a programming language operational semantics. (By context-updates we understand programming language constructs such as goto instructions or function calls.) The context-updates synthesis follows two directions: an overapproximating phase that extracts a set of potential context-update constructs and an underapproximating phase that refines the results of the first step by testing the behaviour of the context-updates constructs produced at the previous phase. We use two experimental semantics that cover two types of language paradigms: high-level imperative and low-level assembly languages and we conduct the tests on standard benchmarks used in avionics.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

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

Slicing is a well established analysis method that takes a program and a slicing criterion (i.e., a program point pc and a set of program variables V) and produces a program slice (i.e., the parts of the program containing language constructs units, usually discriminated based on the sequencing operator, that change the variables in V, directly or indirectly, during the program executions either up to or from the program point pc

). Note that depending on the moment slicing is applied, we have either dynamic slicing—used at the program runtime, and static slicing—used without executing the program. In this paper, we focus on static slicing and we refer it as simply

slicing. Moreover, hereafter we refer the language constructs units, i.e., the syntactic components of the programming language that are separated by sequencing operators, as instructions.

The main idea in program slicing relies on the evaluation of the data flow equations over the control flow graph of the program. Obviously, besides the data-flow, there is a need of additional techniques to help with other language features—[34] gives a comprehensive survey on the standard program slicing techniques applied over different programming language concepts such as standard imperative, pointers, unstructured control flow, and concurrency. Generally, these techniques use the programs’ control flow graphs with various augmentations, e.g., the function calls are usually represented by call-edges [30]. Consequently, any programming language supporting slicing has to be automatically translated into control flow graph based models.

Field and Tip show in [11] a method to derive program slices and dependences from term rewriting systems. This method is applicable to any language with semantics specified as a term rewriting system. Hence, the translation of the programs into their afferent model for slicing is replaced by describing the semantics of the programming language as a rewriting system. Furthermore, the rules in this rewriting system are augmented with wrappers, which maintain the slicing information. In order to compute a program slice, the term representing the program is rewritten with the augmented rewriting system until it reaches the normal form, which contains the slice via the wrappers. This method is tantamount to determining the slice in a dynamic fashion during program execution.

Along the lines of programming language semantics as rewriting systems, we observe an increased interest in defining various languages to cover many programming language paradigms. This desideratum is stated in the rewriting logic semantics project [20], where the programming languages semantics are defined as rewriting systems using Maude, and it is followed by the work in the framework [27]. Maude [7] is a high-level language and high-performance system supporting both equational and rewriting logic computation. Maude modules correspond to specifications in rewriting logic [19], a logic that allows specifiers to represent many models of concurrent and distributed systems. This logic is an extension of membership equational logic [6], an equational logic that, in addition to equations, allows the statement of membership axioms characterizing the elements of a sort. Rewriting logic extends membership equational logic by adding rewrite rules that represent transitions in a concurrent system and can be nondeterministic. In the context of [20], these rules correspond to the execution of the different instructions in our programming language, hence allowing a natural representation for any programming language semantics. As a semantical framework, Maude has been used to specify the semantics of several languages, such as LOTOS [35], CCS [35], and Java [9]. Moreover, the -Maude compiler [28], which is able to translate specifications into Maude, has eased the methodology to describe programming language semantics in Maude.

Our work comes to complement the rewriting logic semantics project by developing static analysis methods, in particular slicing, for programs written in languages with an already defined rewriting logic semantics in Maude. Our approach analyses a given programming language semantics and synthesizes the necessary information for program slicing. We use the results of the syntheses to traverse the program term in order to obtain the program slice. However, we do not execute the program as in [11]. Rather, we construct over the program an augmented control flow graph structure and we use it to obtain the program slice. The novelty, comparing to the standard methods presented in [34], is that we construct the program models in a generic way, for any programming language with a given algebraic semantics.

Our approach is implemented in Chisel111https://github.com/ariesco/chisel, a Maude tool for generic program slicing [26]. Chisel takes a programming language semantics, given as a Maude specification, breaks it into pieces of interest for slicing, and uses these pieces to augment the program term and to produce the model, which is then sliced. For experiments we use two semantics: a semantics for an imperative programming language with functions, WhileFun, and a semantics for the MIPS assembly language. Chisel synthesizes these semantics to extract operators that produce updates at the memory level. These operators are then used to produce necessary information for slicing, e.g., side-effect instructions. The final step of Chisel is the program slicing analysis that takes a program and produces its slice w.r.t. a slicing criterion. Chisel aims to evolve into a framework for generic static slicing.

The main argument for the genericity claim lays in the fact that any programming language paradigm involves a semantic notion of memory/environment which is crucial for slicing and on which we focus our syntheses. Another argument is given by Tip’s survey [34] which presents specialized slicing algorithms for various programming paradigms. However, there is a price to pay for genericity: the slicing precision. Namely, the analyses of the programming language semantics produce supersets of the language constructs involved in slicing. Hence, the loss of precision directly depends on the imprecision of the synthesized language constructs. For producing more accurate synthesis results, we introduce the filtering step based on program testing.

With the current development of Chisel we target sequential imperative code without dynamic allocation that is generated from synchronous designs—a class of applications used in real-time systems, e.g., avionics. The contribution of this paper is presenting the context-updates synthesis component of Chisel, where by context-updates we understand programming language constructs such as goto instructions or function calls. The context-updates synthesis follows two directions: an overapproximating phase when we analyse the language semantics specification to extract a set of potential context-update constructs and an underapproximating phase when we stress-test the semantics to refine the context-updates obtained at the first step. The underapproximating phase, firstly introduced in this paper, is justified by the lack of precision of the overapproximating phase for the context-updates. This lack of precision is most likely due to the laxity of the automatic detection of stack-like memory operators. Note that the class of target languages, i.e., programming languages present in the synchronous compilation chain, does not involve pointers while the arrays are always of fixed size. Since Chisel does not handle programs with pointers yet, we transform the fixed-size arrays into function calls (i.e., we add to the program a function that implements array accesses) so we can use Chisel for slicing industrial benchmarks that contain arrays.

The rest of the paper is organized as follows: in Section 2 we present a comparison with existing works on generic program slicing, in Section 3 we give an overview of the Chisel tool, in Section 4 we describe our method for context-updates synthesis and its integration in Chisel, and in Section 5 we describe the experimental evaluation of selected benchmarks. Section 6 concludes and outlines some future work directions. The complete code of the tool and examples are available at https://github.com/ariesco/chisel.

2 Related work

Program slicing [36] is a standard analysis technique, hence most static analyzers contain some variant of program slicing. Slicing techniques [34]

are classified as static, i.e., the slices are computed without assuming a particular program input, and dynamic, i.e., the test cases determine the program slices.

Standard program analyzers include the necessary infrastructure to compute static slicing in the encoding of the language semantics, the control-flow graph, the dependency relations between program variables, etc. Examples of program slicing tools integrated in program analyzer are, for high-level languages: FramaC [16] for C code, and CodeSurfer [33] and Wala [12] for Java, whereas for low(er) level languages: Giri [29] for LLVM intermediate representation, CodeSurfer/x86 for disassembled x86 executables [4], MCSLICE [31] for Intel IA-32 microcode, and SlicingDroids [15] for Android executables. All these tools translate a program into a model for analysis, then analyze the model. However, the translation phase is particular to the language for which the analyzer is built and takes into account knowledge about the particularities of each language. Chisel aims to unify the translation phase by inferring the particularities of each language from its given algebraic semantics. Through this, we explore the genericity limits of slicing, in particular, and static analysis, in general.

An early work on generic slicing is presented in [8] where the tool compiles a program into a self slicer. Generic slicing is also the focus in [5, 10]. The ORBS tool [5] proposes a technique for dynamic slicing, based on statement deletion. A program slice is constructed iteratively by removing statements from the original program and then checking if the transformation is semantics-preserving w.r.t. the slicing criterion. Their semantics preserving verification phase relies on novel testing techniques [17]. Chisel proposes a complementary technique to the dynamic slicing of ORBS, as it computes static slicing based on in-depth investigation of the formal language semantics. We also use benchmark testing techniques for improving the precision of the context-updates synthesis.

Another generic program slicing technique is proposed in [10] where an algorithm mechanically extracts slices from a common intermediate representation named PIM. The algorithm relies on a well-defined, non-trivial, and language dependent transformation between a language semantics of choice and PIM. The approach in [10] is generic in the sense that notions of static and dynamic slices are represented as constrained slices and various slicing methods are collapsed in a parametric slicing procedure. Chisel integrates now only static slicing and addresses genericity from a different angle: it eliminates the need of a language-dependent translation by working directly on the language semantics.

In rewriting logic, the work in [2] implements dynamic slicing for execution traces of the Maude model checker. The semantics is executed for an initial given state, then dependency relations are computed using a backward tracing mechanism. In comparison, Chisel proposes a static approach built around a formal semantics and with an emphasis on computing slices for programs and not for given traces (e.g., of model checker runs). Also, our proposed algorithm for context-update inference is based on a notion of hypertree, which we introduced and used for side-effects analysis, in [24]. A similar construction to our hypertree, called 2D graph is used in [18] for proving termination of term rewriting systems.

Besides the generic aspect of Chisel, we mainly address in this paper the context-updates discovery component in our framework that is also of interest in the functional programming community. Functional programming proposes richer notions of contexts (and context manipulation) than what we consider in our framework. Briefly, the standard definition of a context as variables in scopes is extended in functional languages in several directions. On one hand, there are high-level constructs such as call/cc - call with current continuation - from the Scheme language [1], where snapshots of the current control states are manipulated as values (e.g., passed as arguments to function calls). One another hand, there are extended notions for contexts to capture security properties, as in the SLam calculus from [13] or parameters of the execution platforms [32, 22]. For example, the contexts are used to track how programs affect an execution environment (e.g., the effect systems [32]) or the complementary approach about how programs depend on the execution environment (e.g., the coeffect systems [22]). In our framework, the context is a first-order variable that could be explicitly or implicitly represented in the programming languages’ semantics. We identify context changes (i.e., context-updates) in a generic manner, directly from a formal language semantics given as rewrite theories. Our context-updates discovery is less particularized as the mentioned related work in functional programming. This is due to the genericity character of our approach, i.e., we do not address a particular type of memory/environment representation as the one in functional programming. Nevertheless, the rich representations of context from functional programming are of interest for our framework in order to specialize the context-updates detection with the inference of types of variables updates during context changes, e.g., different parameter passing styles at function call.

The theoretical ideas underlying Chisel are in [24, 3, 25]. In [24] we describe the methodology for performing intraprocedural slicing, which is improved in [3] by implementing interprocedural slicing. In [25] we introduce an algorithm for inferring the data-flow information to automatically detect how the language constructs work with the memory. A description of the implementation of Chisel could be found in [26]. A preliminary study of the benchmarks used for testing in this paper is presented in [3], but limited to subparts of the code and only evaluated on high-level imperative languages.

3 The Chisel system

We briefly describe in this section the ideas underlying Chisel tool. Chisel aims to advance the generic synthesis of program models from any programming language, provided the algebraic semantics of the language is given as a rewriting system. For now, the analysis of interest is program slicing. Note that the standardized model used by program slicing is an augmented control flow graph, i.e., a set of control flow graphs connected by call edges.

The crucial information used in slicing is related to the data flow: which language constructs produce the data flow and how the data is actually flowing. The main observation we use for Chisel is the fact that side-effects induce an update in the memory afferent to the program. Hence, Chisel first detects the operators used by the semantics to reproduce memory updates. Then, the usage of the memory update operators is traced through semantics up to the language constructs. Any language construct that may produce a memory update is classified as producing side-effects. Moreover, following the direction of the memory updates, we infer also the data flow details (i.e., source-destination) for each side-effect language construct. Finally, the information gathered by Chisel about language constructs is used to traverse the term representing the program and to extract the subterms representing the slice.

Side-Effects Synthesis

Memory Policies Synthesis

Context-Update Synthesis

Language Semantics

Term Analysis

Figure 1: Chisel components: the formal language semantics and the analyses.

The tool works under a few assumptions w.r.t. the programming language semantics specification. Firstly, we assume that is provided as an algebraic specification in rewriting logic. Given the bundle of work in the area of programming language specifications using rewriting logic, as discussed in Section 1, we consider that this first assumption does not impose a restriction on the generality. Secondly, we assume the existence of a certain structure in this semantics. Namely, the instructions are terms of a particular sort (i.e., a type) and the memory/environment/machine on which the programs are running are described by the operators defined in a certain specification module. The idea behind this assumption is the fact that any semantics of a programming language uses an (abstract) memory and some operations over this memory.

We present in Fig. 1 the structure of Chisel: its components and their input-output relations. We briefly address each of the components in the following subsections, except the context-update synthesis, which is the main contribution of the current work and is to be elaborated in the remaining of this paper.

3.1 Memory policies synthesis

Let us denote as the part of that defines some (abstract) form of the memory used during program execution. Our assumption about the structure of the memory is that it connects the variables in the program with their values possibly via a chain of intermediate addresses. We define a memory policy as a particular type of operators specified using , where and are standardly denoted as the arity and, respectively, co-arity of . (Note that denotes a sort while denotes a list of sorts.) For example, a memory-read is the set of operators in that contain in their arity the sort for variables and for memory and in their co-arity the sort for values. A memory-write operator contains in its arity sorts for memory, variables, and values, and in its co-arity the memory sort. Also, the rules defining this operator change the memory by updating the variable with the value. In [25] we present the analyses of for read/write memory policies that produce the set of operators that manipulate the memory according to the given policy.

3.2 Side-effect synthesis

Let us denote by the part of that defines the operators representing the programming language constructs, i.e., language instructions. The first inference that Chisel has to make is regarding which elements in modify the program variables. Since in the memory the program variables are connected to their values, determining constructs that modify a variable essentially means tracking the effects of an instruction over its variable component until reaching the memory level. Hence, we name side-effect language constructs those operators in that produce a memory-write over some of its variable component.

To determine the subset of that may be side-effect constructs, Chisel identifies the set of rules in containing in the left-hand side (as a subterm) the operators. The side-effect synthesis starts with the rules in and constructs a hyper-tree whose nodes are sets of rewrite rules and edges are unification based dependencies between these rules. Chisel discriminates the side-effect constructs by following the paths in this hyper-tree from the root to the leaves. The paths leading to leaves that contain rules already classified by the memory policy phase as memory-writes are signalling the side-effect constructs. This part of Chisel is presented in [24].

The next phase of the side-effect synthesis consists in using the constructed to determine the data flow (source-destination) produced by the side-effect constructs. Essentially, at this phase, Chisel trickles-up the paths of the hyper-tree , starting from the leaves up to the root. Namely, at the leaves level we identify the variable subterm as destination and the value as the source. This identification is based on the read/write memory policy phase. The information regarding the source-destination relation between these two subterms (i.e., value/variable) is propagated up on each path in by a backwards inference of the unifications of these subterms. When reaching the root of , the value at the memory level is hooked to the sources subterms and the variable to the destination subterms. Hence, we determine the data flow induced by each side-effect construct and we describe this as a part of [25].

Note that side-effects synthesis determines an over-approximation of the side-effect constructs. The data flow inference phase only enriches each of the already discovered side-effects constructs with key information for the program slicing, i.e., the source-destination direction of the flow of data.

3.3 Term analysis

The algorithm for slicing a program takes as input a slicing criterion consisting in a set of program variables. In this step, Chisel takes the tree representing the program term and traverses it repeatedly. Each traversal phase adds new elements to the set and the process is repeated until the set stabilizes. While traversing , whenever a side-effect construct is encountered, if the destination of this construct is from then all the source variables are added to . Also, whenever a context-update construct is encountered, the traversal of is redirected towards the ’s subtree whose root matches a particular subterm of the context-update construct. At the end of the traversal, the program slice is given by the skeleton of containing the subtrees representing the instructions that produced changes to the set .

4 The context-update inference algorithm

In this section we present our approach towards discovering context-update constructs in the programming language under consideration. We start by setting some notation and defining the intuitive ideas. Note that in the followings we use notation introduced in the previous section.

Firstly, we denote the list of elements from –the language instructions’ sort–obtained by a preorder traversal of –the tree associated to the program . We define as context-update constructs (context-updates for short) those operators in that, during program execution using , produce changes to the list . For example, function calls and gotos are context-update constructs. We denote by context-updates synthesis the strategy of deducing, based on the language semantics , an overapproximation of the set of context-update instructions.

The methodology we propose for context-updates synthesis follows the same strategy as the one for side-effects described in the previous Sections 3.1 and 3.2. Namely, we firstly apply sort-based patterns to the memory module in in order to identify stack structures/memory operators or, short, memory-stacks (Section 4.1). Secondly, using the memory-stacks we traverse the hyper-tree to discover the set of language constructs that directly use the memory-stacks (Section 4.2). Note that is an overapproximation of the context-updates since our target semantics describe context-free languages of either high or low level. As the context-free languages need some stack representation and we trigger our context-updates synthesis by an initial phase that discovers memory-stack patterns in . Finally, in order to make the set more accurate we use a refinement step that, based on the execution of benchmarks, partitions the subset in three: the function call constructs, the goto constructs, and the residue constructs that are present in due to the overapproximations in the first two steps (Section 4.3).

4.1 Memory-stack policy

The memory-stack policy determines rules in constructing stack-like structures at the memory level. The strategy applied for memory-stack policy is similar to the strategy described in Section 3.1. Namely, we have two patterns we search for: explicit and implicit. The explicit memory-stack policy where we determine non-commutative memory operators or that have a subsorted arity and all the rules describing them either add or substract one element. The implicit pattern uses the conditional rules over the language semantics to produce memory-stacks. The implicit pattern is produced by the Maude’s evaluation semantics that uses a an evaluation stack for conditional rules. Namely, the evaluation of the conditional rule’s body (i.e., the statement between the crl and if keywords) is postponed until the evaluation of the rule’s condition (i.e., the statement after if keyword) is completed.

Example 1

We present in this example the memory specification for WhileFun–an imperative language with assignment, conditional, loops, local variables, an input/output buffer, and function calls [14, 3]. Assuming we have defined the syntax for the language in a module WHILE-SYNTAX (which includes definitions for variables, Boolean values, and numeric values), the module MEMORY imports this module and defines the sorts Env for the environment, which maps variables to values, and ESt for a stack of environment, which will be used when a new context is required:

fmod MEMORY is
  pr WHILE-SYNTAX .
  sorts Env ESt .
  subsort Env < ESt .
  ...

where the subsort indicates that a single environment states for a singleton stack, i.e., the environment type is a subtype of the environments’ stack . Constructors of these sorts are defined by using op and the attribute ctor. In this case, we define the empty environment (mt); a single assignment, which receives a variable and a value (underscores are placeholders); and the composition of environment, defined with empty syntax and defined as commutative and associative and having mt as identity:

  op mt : -> Env [ctor] .
  op _=_ : Variable Value -> Env [ctor] .
  op __ : Env Env -> Env [ctor comm assoc id: mt] .

Similarly, the stack is built by putting together stacks with the _|_ operator:

  op _|_ : ESt ESt -> ESt [ctor assoc] .

The operator _|_ follows the explicit memory-stack policy and it will be used in the context-update synthesis, as described in Example 2 from Section 4.2. The memory module also contains functions for variables’ update, variables’ look-up, and new variables allocation. Below we show Chisel commands and the results for the memory-stack policy applied on WhileFun:

Maude> (memory inferences .)
ESt RWBUF
Maude> (context update sorts .)
ESt
Maude> (memory-stack ops .)
_|_

Namely, memory inferences command produces the sorts that agree with memory-stack policy: ESt–the environment stack sort defined in the MEMORY module–and RWBUF–the sort defining the memory buffer that handles the results of the read/write instructions in WhileFun. The context update sorts command keeps from the memory-stack sorts only the sorts that produce context changes, as we describe next, in Section 4.2.

4.2 Context-updates synthesis

The synthesis of a set of constructs that may produce context-updates relies on the hyper-tree constructed for the operators in and is similar with the side-effects synthesis described in Section 3.2 and [24]. The difference here is the fact that at the leaves level we now use a different memory policy (the memory-stack policy) to filter the paths leading to context-updates. The algorithm implementing this in Chisel is defined by the operator traverseHypertree given in Fig. 2:

  op traverseHypertree : Module QidSet TermList ContextUpdates HypertreeTraversalResult
                         -> HypertreeTraversalResult .
  eq traverseHypertree(M, none, TL, CU, HTR) = HTR .
  ceq traverseHypertree(M, Q ; QS, TL, CU, HTR) = traverseHypertree(M, QS, TL, CU, HTR’)
   if Q in CU /\
      HTR’ := add2orange(Q, HTR) .
  ceq traverseHypertree(M, Q ; QS, TL, CU, HTR) = traverseHypertree(M, QS, TL, CU, HTR)
   if traversed?(Q, HTR) .
  ceq traverseHypertree(M, Q ; QS, TL, CU, HTR) =
            if allOrange?(HTR’) and not emptyHypernode(M, COND, (T, TL))
            then add2orange(Q, HTR’)
            else add2olive(Q, HTR’)
            fi
   if COND := getCondition(M,Q) /\ not Q in CU /\ not traversed?(Q,HTR) /\ T := getLHS(M,Q) /\
      HTR’ := traverseCond(M, COND, (T, TL), CU, setAllOrangeVar(true, HTR)) .

  op traverseCond : Module Condition TermList ContextUpdates HypertreeTraversalResult
                    -> HypertreeTraversalResult .
  eq traverseCond(M, nil, TL, CU, HTR) = setAllOrangeVar(false, HTR) .
  eq traverseCond(M, T = T’ /\ COND, TL, CU, HTR) = traverseCond(M, COND, TL, CU, HTR) .
  eq traverseCond(M, T := T’ /\ COND, TL, CU, HTR) = traverseCond(M, COND, TL, CU, HTR) .
  eq traverseCond(M, T : S /\ COND, TL, CU, HTR) = traverseCond(M, COND, TL, CU, HTR) .
  ceq traverseCond(M, T => T’ /\ COND, TL, CU, HTR) = combineHypernodes(HTR’, HTR’’)
   if TV := freshTerm(T) /\
      QS := getRulesUnifying(M, TV, getRls(M), TL) /\
      HTR’ := traverseHypertree(M, QS, TL, CU, HTR) /\
      HTR’’ := traverseCond(M, COND, TL, CU, setAllOrangeVar(true, HTR’)) .
Figure 2: The traverseHypertree operator in Chisel.

The operator in Fig. 2 computes the set of basic syntactic language constructs that may be context-updates, by inspecting the conditions and the right-hand side of each rewrite rule in represented here as Q, i.e., the rule label. The operator unfolds the rewrite rules into the hyper-tree with children nodes representing lists of rules that unify with subterms of Q’s conditions (each subterm of Q unifies with a particular node). The traversalHypertree operator goes horizontally in if there is no subtree rooted in the current Q node. Otherwise, when Q is the root of a subtree in (e.g., when the rule Q is conditional), the traversal goes vertically via the operator traverseCond. The traversalHypertree operator assigns each rule label Q to a particular set, either orange or olive, where these sets are defined as follows:

The orangeSet contains Qs that are the root of a subtree containing context-updates while oliveSet is context-updates free. Note that the termination of the algorithm in Fig. 2 is ensured by the fact that the specification has a finite number of rules, and that any rule in that was already added to either orange or olive set is not unfolded anymore. We give next an example that provides the intuition about the synthesis process.

CallF AsR Inc1 Inc2 SdE IfR1 WriteR ReadR1ReadR2

_ _<_,_>asgP1asgP2_|_<_,_,_,_>

rmv1rmv2_|_
Figure 3: The hyper-tree constructed for WhileFun.
Example 2

The first part of the hyper-tree , constructed for WhileFun semantics, is depicted in Fig. 3. The memory-stack operator discovered here at the leaves level is _|_ that is obtained by the explicit memory-stack policy. The root of contains the language constructs where we show first CallF the rule label that specifies the semantics of a function call such as:

  crl [CallF] :
   < Call fn(actPrms), st, rwb, fs > => < skip, st’’, rwb’, fs >
   if fn(Prms){ C } fs’ := fs /\
      < actPrms, st > => vals /\
      st’ := assignPrms(actPrms, Prms, st | mt) /\
      < C, st’, rwb, fs > => < skip, st’’ | lenv’, rwb’, fs >  .

The first condition in the rule CallF extracts the function definition from the function set fs by means of a matching condition; the second condition evaluates the arguments passed to the function; the third condition uses the function assignPrms (described below) to bind the parameters to the values previously obtained; and the fourth condition evaluates the body of the function in the new stack of environments.

  op assignPrms : ExpL VarL ESt -> ESt .
  eq [asgP1]  : assignPrms(nv, nv, ro) = ro .
  eq [asgP2]  : assignPrms((N,EL), (X,VVs), mu | ro) =
                assignPrms(EL, VVs, mu | remove(ro, X) (X = N)) .

The function assignPrms is in charge of assigning the appropriate values to the parameters of a function call. It receives a list of expressions, a list of variables, and a stack of environments as arguments and traverses the lists removing the previous value associated to the variable at the top of the stack and binding it to the new one.

4.3 Context-updates refinement

For the refinement step we use a modified version of the Maude testing tool presented in [23], which generates test cases for Maude functional modules and executes these tests while checking the conformance of their result w.r.t. a given specification . In our case, is defined over the execution trace as defined next.

Given the programming language semantics and a program , which is associated in with a term with a tree representation , we define the flattening of into a list of instructions (i.e., unit elements in ) obtained by the preorder traversal of (i.e., the listing of the program’s code instructions). Given a set of execution traces we denote its elements by , i.e., an execution path of w.r.t. . Furthermore, we denote by the filtering of w.r.t. the language constructs . We use the standard notation for , namely represents the length of the path, while , represents the -th element of the path. Note that is , the empty execution list. We also denote by the set function definitions in :

where represents the -th element of the list and is the set of program constructs representing function declarations.

Definition 1

The property w.r.t. is defined as follows:

Hence the three sets (the function call constructs), (the goto constructs), and (the residue constructs) are obtained from by a discrimination process based on the analysis of testing traces. Namely, the residues are constructs that execute always in programs’ sequential order; the gotos and function calls are constructs that break the sequential order for either jumping inside the current function body, or to another function, respectively. Note that if the sets , , and do not form a partition we use the remaining elements in to signal counterexamples for the context-updates inference phase. In the next section we describe the benchmark tests we used for the experimental semantics WhileFun and MIPS.

5 Experiments in Chisel

We apply Chisel, extended with the synthesis algorithm for context-updates on a standard benchmark for real-time systems called PapaBench [21]. We consider, in our experimental evaluation, both formal semantics of WhileFun and MIPS. PapaBench is a code snapshot extracted from an actual real-time designed for Unmanned Aerial Vehicle (UAV). It consists of two communicating applications, a command management called fly_by_wire and a navigation management called autopilot. Both applications have a number of inter-dependent tasks which are executed in a control loop, at different frequencies. Structurally, fly_by_wire has five tasks, named from T1 to T5 and autopilot has eight tasks, from T6 to T13. Moreover, each application serves three interrupts, which are not of concern for the current tool evaluation. The tasks are summarized in Fig. 4 (left) and their inter-dependencies shown in the same figure (right). PapaBench application has two modes–manual and automatic. In the manual mode, the radio command (T1) of fly_by_wire is executed, sending data (T2) to autopilot which analyzes and sends back information (T6, T7, T8) to fly_by_wire for processing and issuing commands (T3, T4). The automatic mode is triggered in autopilot by the GPS communication (T9) and enables navigation, altitude and climb control (T10, T11, T12) before stabilization (T7). T5 of fly_by_wire and T13 of autopilot handle failure checking and respectively parameter reporting.

Tasks : fbw and autopilot T1   - receive_radio_commands T2   - send_data_to_autopilot T3   - receive_data_from_autopilot T4   - transmit_servos T5   - check_failsafe T6   - manage_radio_commands T7   - control_stabilization T8   - send_data_to_fbw T9   - receive_gps_data T10 - control_navigation T11 - control_altitude T12 - control_climb T13 - manage_reporting
Figure 4: PapaBench: The tasks and their dependencies
Name # Funs # Calls LOC red (%) LOC red (%)
 (WhileFun)  (WhileFun)  (MIPS)  (MIPS)
14 18 103 72.8 % 396 44.4 %
21 80 225 73.3 % 779 36.3 %
41 110 638 91.1 % 1913 41 %
10 26 119 76.5 % 534 36.2 %
9 9 59 69.5 % 329 44.4 %
9 24 82 76.5 % 501 43.6 %
9 14 50 61.5 % 235 34.5 %
7 22 66 67 % 453 51 %
95 214 1384 92 % 5639 41.5 %
36 71 306 77.2 % 1329 54 %
9 13 57 70 % 426 42 %
7 15 54 69.2 % 219 38 %
15 30 87 75 % 617 36.5 %
18 27 102 71.1 % 1002 42.2 %
3 2 15 63.4 % 90 70.6 %
4 3 49 66.2 % 363 50 %
37 93 240 79.7 % 1535 42 %
Figure 5: Chisel performance on PapaBench benchmark

PapaBench is organized in modes and tasks, coordinated by a set of global variables. We apply Chisel on two programming levels: the original imperative code and the binary code obtained after disassembling. In this way we aim to study the analyzability and traceability properties of PapaBench, e.g., isolate and quantify different functionalities within each task, as well as the inter-task behavior between communicating tasks. We report the results of Chisel as reduction in the number of instructions (LOC). Next, we elaborate on experimentation (i.e., platform, organization, test cases) and its results, as summarized in Fig. 5.

We conduct our experiments on the following settings: we run Chisel with Maude (and Full-Maude) 2.7 on a MacBook Pro 2.5 GHz, 4GB RAM, with PapaBench version 0.4 (for the WhileFun code) and the gcc 4.7.1 cross-compiler to obtain MIPS code (and with sufficient traceability to check the corresponding program slices at the high- and low-levels).

We organize the benchmark as follows, in Fig. 5: each of the 13 tasks (the rows T1 to T13), the core functionalities of fly_by_wire and autopilot (the rows scheduler_fbw and periodic_auto), and the complete PapaBench benchmark (the rows fly_by_wire and autopilot). These latter four functionalities were introduced in [26] and included here for completion purposes. Note that in [26] the context-updates were manually introduced for each language while here we detect them automatically. The context-updates synthesis phase produces exact results for WhileFun while for MIPS the overapproximation at the synthesis phase is too large (the synthesized context-updates for MIPS include most of the language instructions). Hence, the testing phase, which is underapproximating the synthesized context-updates, is essential for context-updates in MIPS. We employ random testing on the currently described benchmarks and we reduce the context-updates for MIPS to the exact set. Hence, the results reported in the Fig. 5 coincide with the results obtained in [26] where the context-updates were manually provided. We quantify the number of functions and function calls (columns #Funs and respectively #Calls), the code size (LOC) and the slicing reduction factor, red(%) for both WhileFun and MIPS programs.

The reduction factor captures the slicing performance w.r.t., the original code on both WhileFun and MIPS variants. We measure the slicing performance in the following way:

  • The rows with the full benchmark (autopilot and fly_by_wire) and its core functionalities (scheduler_fbw and periodic_auto): the slicing procedure considers as slicing criteria sets of global variables used to activate modes and inter-task communication. The red(%) shows the reduction percentage resulting from program slices size over the reference (original) code size.

  • The rows corresponding to each task T1 to T13 (with the exception of T11–control_altitude–which is very small, but included for completeness purposes): the slicing procedure is based on 7 slicing criteria designed to measure several aspects of a task code. These criteria correspond to:

    • global functionality, i.e., variable(s) responsible for the task functionality;

    • mode split, i.e., global and local variable(s) related to modes involved in the task main function;

    • inter-tasks parameters, i.e., variable(s) that emphasize the communication between tasks in Fig. 4 (e.g., T5-T4 for T5, T2-T6-T1-T3 for T2);

    • global params. impact, i.e., global variable(s) used in performing the respective task functionality;

    • effect on inter-procedural behavior, i.e., global impact of function calls;

    • effect on the communication for control navigation and climb tasks (T10 and T12) and arrays to measure the penalty incurred when transforming array operations into function calls (for T1-T9 and T13), and local impact of specific function calls.

Chisel, when applied on WhileFun programs performs well, partly because of the inter-procedural analysis and partly because the code structure is mode-based. The results for WhileFun are reported in Fig. 5 (column red(%)WhileFun). On the other hand, we report lower percentages for MIPS code, as shown in Fig. 5 (column red(%)MIPS) because of several reasons. First, the current version of Chisel does not follow through the memory addresses. Second, any function call in a small sized function involves setting the function stack with registers global and stack pointer, which end-up dominating the code size and yielding longer slices. Third, as reported in [31], in general slicing binary code could result in longer slices because of the indirect side-effects via register flags. However, Chisel slicing on MIPS code stays generic and it is work in progress to employ a slicing procedure specialized for low-level languages to produce more precise slices. Using the criteria 6- and 7-, we measure the improvement of an inter-procedural analysis for MIPS that amounts to an additional 20%-25% reduction.

PapaBench is used to evaluate the worst-case execution time (WCET) analysis and to experiment different scheduling models. In these contexts and with respect to the considered benchmark, we used the program slices computed with Chisel for several purposes. For example, we perform the intersection of program slices obtained on criteria such as functionality modes and we identify what are the shared and/or individual behaviors as well as communication patterns at the code level (in particular on WhileFun code). Also, we use the program slices to discover the computationally intensive modes which in turn would impact the task scheduling and the accuracy of the WCET analysis. In this latter case, it is a well-known WCET estimation technique to evaluate the code generated from synchronous designs in two phases: the initialization and the rest.

6 Concluding remarks and future work

In this paper we have presented a generic synthesis method for context-updates constructs, from given semantics of programming languages written in Maude. The synthesis strategy follows three stages: the memory policy, the context-updates overapproximations, and the overapproximation refinement. We also integrated our method in Chisel, a Maude tool that can perform generic program slicing. We experimented our extended Chisel with different semantics: WhileFun (imperative) and MIPS (assembly), both of them with different variations, e.g., different memory models and data flow styles. We have also designed test programs to evaluate the efficiency of the produced slices. These experiments correspond to Unmanned Aerial Vehicle applications, which prove that this technique can be applied to real-time programs. Note that for the moment we use these benchmarks in the refinement step.

As ongoing work we focus on a more complex strategy for the refinement step by using more evolved testing strategies. For future work, we plan to extend the language with pointers, hence supporting more complex memory policies based on a more refined memory model. Finally, our aim is to introduce concurrency in the framework so that we can cover and test out proposed methodology on a larger and significant class of programming languages.

References

  • [1] H. Abelson and G. J. Sussman. Structure and Interpretation of Computer Programs. MIT Press, 1985.
  • [2] M. Alpuente, D. Ballis, F. Frechina, and J. Sapina. Combining runtime checking and slicing to improve Maude error diagnosis. In Logic, Rewriting, and Concurrency, volume 9200 of LNCS, pages 72–96. Springer, 2015.
  • [3] I. M. Asavoae, M. Asavoae, and A. Riesco. Towards a formal semantics-based technique for interprocedural slicing. In iFM 2014, volume 8739 of LNCS, pages 291–306. Springer, 2014.
  • [4] G. Balakrishnan, R. Gruian, T. W. Reps, and T. Teitelbaum. CodeSurfer/x86-a platform for analyzing x86 executables. In CC, volume 3443 of LNCS, pages 250–254. Springer, 2005.
  • [5] D. Binkley, N. Gold, M. Harman, S. Islam, J. Krinke, and S. Yoo. ORBS: Language-independent program slicing. In FSE14, pages 109–120, 2014.
  • [6] A. Bouhoula, J.-P. Jouannaud, and J. Meseguer. Specification and proof in membership equational logic. Theoretical Computer Science, 236:35–132, 2000.
  • [7] M. Clavel, F. Duran, S. Eker, P. Lincoln, N. Marti-Oliet, J. Meseguer, and C. Talcott. All About Maude, volume 4350 of LNCS. Springer, 2007.
  • [8] S. Danicic and M. Harman. Espresso: A slicer generator. In SAC, pages 831–839, 2000.
  • [9] A. Farzan, F. Chen, J. Meseguer, and G. Rosu. Formal analysis of Java programs in JavaFAN. In CAV, pages 501–505, 2004.
  • [10] J. Field, G. Ramalingam, and F. Tip. Parametric program slicing. In POPL, pages 379–392. ACM Press, 1995.
  • [11] J. Field and F. Tip. Dynamic dependence in term rewriting systems and its application to program slicing. Information & Software Technology, 40(11-12):609–636, 1998.
  • [12] S. J. Fink, E. Yahav, N. Dor, G. Ramalingam, and E. Geay. Effective typestate verification in the presence of aliasing. In ISSTA, pages 133–144. ACM, 2006.
  • [13] N. Heintze and J. G. Riecke. The slam calculus: Programming with secrecy and integrity. In POPL, pages 365–377, 1998.
  • [14] M. Hennessy. The Semantics of Programming Languages: An Elementary Introduction Using Structural Operational Semantics. John Wiley & Sons, 1990.
  • [15] J. Hoffmann, M. Ussath, T. Holz, and M. Spreitzenbarth. Slicing droids: program slicing for smali code. In SAC, pages 1844–1851. ACM, 2013.
  • [16] F. Kirchner, N. Kosmatov, V. Prevosto, J. Signoles, and B. Yakobowski. Frama-c: A software analysis perspective. Formal Asp. Comput., 27(3):573–609, 2015.
  • [17] W. B. Langdon, S. Yoo, and M. Harman. Inferring automatic test oracles. In ICSE, pages 5–6, 2017.
  • [18] S. Lucas, J. Meseguer, and R. Gutierrez. Extending the 2D dependency pair framework for conditional term rewriting systems. In LOPSTR, volume 8981 of LNCS, pages 113–130, 2014.
  • [19] J. Meseguer. Conditional rewriting logic as a unified model of concurrency. Theoretical Computer Science, 96(1):73–155, 1992.
  • [20] J. Meseguer and G. Rosu. The rewriting logic semantics project. Theoretical Computer Science, 373(3):213–237, 2007.
  • [21] F. Nemer, H. Casse, P. Sainrat, J. P. Bahsoun, and M. D. Michiel. Papabench: a free real-time benchmark. In WCET, 2006.
  • [22] T. Petricek, D. A. Orchard, and A. Mycroft. Coeffects: a calculus of context-dependent computation. In ICFP, pages 123–135, 2014.
  • [23] A. Riesco. Test-case generation for Maude functional modules. In T. Mossakowski and H. Kreowski, editors, WADT, volume 7137 of LNCS, pages 287–301. Springer, 2010.
  • [24] A. Riesco, I. M. Asavoae, and M. Asavoae. A generic program slicing technique based on language definitions. In WADT 2012, volume 7841 of LNCS, pages 248–264, 2013.
  • [25] A. Riesco, I. M. Asavoae, and M. Asavoae. Memory policy analysis for semantics specifications in Maude. In LOPSTR, volume 9527 of LNCS, pages 293–310. Springer, 2015.
  • [26] A. Riesco, I. M. Asavoae, and M. Asavoae. Slicing from formal semantics: Chisel. In FASE, pages 374–378, 2017.
  • [27] G. Rosu and T. F. Serbanuta. An overview of the K semantic framework. Journal of Logic and Algebraic Programming, 79(6):397–434, 2010.
  • [28] V. Rusu, D. Lucanu, T. Serbanuta, A. Arusoaie, A. Stefanescu, and G. Rosu. Language definitions as rewrite theories. Journal of Logical and Algebraic Methods in Programming, 85(1):98–120, 2016.
  • [29] S. K. Sahoo, J. Criswell, C. Geigle, and V. S. Adve. Using likely invariants for automated software fault localization. In ASPLOS, pages 139–152. ACM, 2013.
  • [30] M. Sharir and A. Pnueli. Two approaches to interprocedural data flow analysis. New York Univ. Comput. Sci. Dept., New York, NY, 1978.
  • [31] V. Srinivasan and T. W. Reps. An improved algorithm for slicing machine code. In OOPSLA, pages 378–393, 2016.
  • [32] J. Talpin and P. Jouvelot. The type and effect discipline. In LICS, pages 162–173, 1992.
  • [33] T. Teitelbaum. CodeSurfer. ACM SIGSOFT Software Eng. Notes, 25(1):99, 2000.
  • [34] F. Tip. A survey of program slicing techniques. Journal of Programming Languages, 3(3):121–189, 1995.
  • [35] A. Verdejo and N. Marti-Oliet. Executable structural operational semantics in Maude. Journal of Logic and Algebraic Programming, 67:226–293, 2006.
  • [36] M. Weiser. Program slicing. In ICSE, pages 439–449. IEEE Press, 1981.