• Nenhum resultado encontrado

Preventing Atomicity Violations with Contracts

N/A
N/A
Protected

Academic year: 2019

Share "Preventing Atomicity Violations with Contracts"

Copied!
86
0
0

Texto

(1)

Diogo Miguel Gaspar de Sousa

Licenciado em Engenharia Informática

Preventing Atomicity Violations

with Contracts

Dissertação para obtenção do Grau de Mestre em Engenharia Informática

Orientador:

João Lourenço,

Prof. Auxiliar, Universidade Nova de Lisboa

Co-orientadora:

Carla Ferreira,

Prof

a

. Auxiliar, Universidade Nova de Lisboa

Júri:

Presidente: Profa. Armanda Rodrigues Universidade Nova de Lisboa

Arguente: Prof. António Menezes Leitão Instituto Superior Técnico

Vogal: Prof. João Lourenço

(2)
(3)

iii

Preventing Atomicity Violations with Contracts

Copyright cDiogo Miguel Gaspar de Sousa, Faculdade de Ciências e Tecnologia,

Uni-versidade Nova de Lisboa

(4)
(5)
(6)
(7)

Acknowledgements

I would like to thank my advisers João Lourenço and Carla Ferreira for their guidance, support, and patience. Lourenço, in particular, has guided me for a few years, and I am thankful for everything I have learned from him in this journey. I also thank Ricardo Dias, Tiago Vale, and João Leitão, who also played a key role in this thesis or preceding, related, projects.

I thank the Departamento de Informática, Faculdade de Ciências e Tecnologia of the Universidade Nova de Lisboa for providing me an amazing environment with a true sense of community. I also extend my gratitude to the following institutions for their financial support: Centro de Informática e Tecnologias da Informação of the FC-T/UNL; and Fundação para a Ciência e Tecnologia in the research project Synergy-VM (PTDC/EIA-EIA/113613/2009), and again, the Departamento de Informática and Facul-dade de Ciências e Tecnologia of the UniversiFacul-dade Nova de Lisboa.

A huge “thank you” to my family. I thank my father, José Sousa, that inspired me to learn and be curious, to make me fall in love with science, and to have the knowledge and patience to answer my uncountable questions about life, the universe, and everything. To my mother, Anabela Gaspar, for the understanding and for always being there for me. To my sister, Mafalda Sousa, whose cheerfulness make my visits to Leiria even better. To my grandparents, Arnaldo Gaspar, Maria Cardeira, Virgílio Sousa and Amélia Rascão, who gave me a great deal of support.

(8)
(9)

Abstract

Concurrent programming is a difficult and error-prone task because the programmer must reason about multiple threads of execution and their possible interleavings. A con-current program must synchronize the concon-current accesses to shared memory regions, but this is not enough to prevent all anomalies that can arise in a concurrent setting. The programmer can misidentify the scope of the regions of code that need to be atomic, re-sulting in atomicity violations and failing to ensure the correct behavior of the program. Executing a sequence of atomic operations may lead to incorrect results when these op-erations are co-related. In this case, the programmer may be required to enforce the sequential execution of those operations as a whole to avoid atomicity violations. This situation is specially common when the developer makes use of services from third-party packages or modules.

This thesis proposes a methodology, based on the design by contract methodology, to specify which sequences of operations must be executed atomically. We developed an analysis that statically verifies that a client of a module is respecting its contract, allowing the programmer to identify the source of possible atomicity violations.

Keywords: Atomicity Violation, Concurrency, Thread Safety, Design by Contract,

(10)
(11)

Resumo

A programação concorrente é um tarefa difícil e propensa a erros visto que o progra-mador tem de ter em conta os váriosthreadsde execução e as suas possíveis intercalações.

O programa tem de sincronizar os acessos concorrentes a regiões de memória partilhada, mas isto não é suficiente para evitar todas as anomalias que podem ocorrer num cená-rio concorrente. O programador pode identificar incorretamente o âmbito das regiões de código que precisam de ser atómicas, resultando em violações de atomicidade que levam a um comportamento incorreto do programa. Correr uma sequencia de operações atómicas pode produzir resultados incorretos quando existe uma correlação entre essas operações. O programador pode ter que fazer a execução sequencial dessas operações atómicas como um todo para evitar violações de atomicidade. Esta situação é especial-mente comum quando o programador usa operações de pacotes ou módulos de terceiros. Esta tese propõe uma metodologia baseada na programação por contrato para espe-cificar sequências de chamadas que devem ser executadas de forma atómica. Apresenta-mos uma analise que verifica estaticamente que um cliente de um módulo está a respeitar o seu contrato, permitindo ao programador identificar a causa de potenciais violações de atomicidade.

Palavras-chave: Violações de Atomicidade, Concorrência,Thread Safety, Programação

(12)
(13)

Contents

1 Introduction 1

1.1 Context & Motivation. . . 1

1.2 Problem . . . 2

1.3 Proposed Approach . . . 3

1.4 Contributions . . . 3

1.5 Publications . . . 4

1.6 Outline . . . 4

2 Related Work 7 2.1 Concurrent Programs Correctness . . . 7

2.1.1 Race Conditions. . . 9

2.1.2 Detecting Atomicity Violations . . . 10

2.2 Design By Contract . . . 14

2.3 Program Analysis . . . 16

2.3.1 Static and Dynamic Analysis . . . 16

2.3.2 Program Representation . . . 17

2.3.3 Types of Static Code Analysis . . . 20

3 Methodology 23 3.1 Analysis Overview . . . 23

3.2 Contract Specification . . . 24

3.3 Extracting the Behavior of a Program. . . 25

3.4 Contract Verification . . . 29

3.5 Extending the Analysis . . . 33

3.5.1 Contracts with the Kleene Star . . . 33

3.5.2 Contracts with Parameters . . . 34

(14)

4.2 Atomically Executed Methods. . . 38

4.3 Parser . . . 38

4.4 Optimizations . . . 40

4.5 Using Gluon . . . 42

5 Evaluation 45 5.1 Validation . . . 47

5.2 Performance . . . 48

6 Conclusion 51 6.1 Concluding Remarks . . . 51

6.2 Future Work . . . 52

A Appendix 59 A.1 Account . . . 59

A.2 Allocate Vector . . . 60

A.3 Arithmetic Database . . . 61

A.4 Connection . . . 62

A.5 Coordinates’03. . . 62

A.6 Coordinates’04. . . 63

A.7 Elevator . . . 64

A.8 Jigsaw. . . 65

A.9 Knight . . . 66

A.10 Local . . . 66

A.11 NASA. . . 68

A.12 Store . . . 68

A.13 String Buffer . . . 69

A.14 Under-Reporting . . . 70

(15)

1

Introduction

1.1

Context & Motivation

The clock frequency of processors stalled in 2005. Increasing the clock speed of a pro-cessor was sustained by advancements in micropropro-cessor manufacturing, in particular reducing power consumption and improving the efficiency of the processor, which is a consequence of making smaller transistors. Subsequently the trade-off between power consumption and clock frequency became more and more expensive. This lead to design of multicore processors, which became a viable way to continue to improve the comput-ing power of processors.

In order for a program to take advantage of multiprocessors it must be written in a concurrent paradigm. To do so the programmer must identify which computational steps are worth executing in separate threads of execution of that program. This allows the operating system’s scheduler to assign threads of execution to different cores of the processor, thus achieving true parallelism, and reducing the real execution time of the program.

Concurrent programming introduces new challenges that do not arise in sequential programs and that can compromise the correctness of the program. One of these prob-lems are data races, where multiple threads of execution of a process access the same

(16)

Listing 1Program containing an atomicity violation (left), and a solution for that anomaly

(right).

1 void schedule_event(DateTime dt,

2 Event e)

3 {

4 if (calendar.isFree(dt)) 5 calendar.schedule(dt,e); 6 }

1 void schedule_event(DateTime dt,

2 Event e)

3 {

4 atomic

5 {

6 if (calendar.isFree(dt)) 7 calendar.schedule(dt,e);

8 }

9 }

A concurrent program free of data races can still contain concurrent-related anoma-lies. For example, an operation may be intended to be atomic, but is implemented in two separate atomic regions. While executing that operation another thread may read an intermediate value between the execution of the two atomic phases, possibly obtaining an inconsistent result. Programs that use a modular design, such as in an object-oriented paradigm, are particularly prone to these errors, since the program will compose opera-tions offered by a module1. Even if these operations are atomic, the programmer must be

careful to compose them in the right way, and their composition may require additional synchronization.

1.2

Problem

One of the most common concurrency related anomalies are caused by atomicity vi-olations [LPSZ08]. Atomicity violations are anomalies where the atomic regions were wrongly or incompletely identified by the programmer, and the program requires ad-ditional synchronization to ensure its correctness. This may be caused by the complete lack of synchronization in critical regions or if the scope of some synchronized regions is insufficiently small and must be broader to ensure that the program has the intended semantics.

Consider the example in Listing1(left), where we use a calendar module, modeled as an object of the classDateTime, that have methods to check if a time slot is free (isFree())

and to schedule an event in a time slot (schedule()). Here the programmer defined an

auxiliary method to schedule events without they ever overlap. The method checks if the requested time slot if free on the calendar, and if so, the event is scheduled. Even if all methods in the calendar class are atomic, the methodschedule_event()can give an

incorrect result, by double booking the same slot with two different events. This anomaly can happen if two threads run this method concurrently. Figure1.1 represents a thread scheduling where two events (ea andeb) were scheduled by concurrent threads at the

same time slot, contradicting the expected behavior of this method.

(17)

1. INTRODUCTION 1.3. Proposed Approach

Threadt

isFree(dt) schedule(dt,ea)

Threadt′

isFree(dt) schedule(dt,eb)

Figure 1.1: Example of concurrent thread scheduling leading to an atomicity violation.

The atomicity violation shown here is common when composing operations of a mod-ule, even if its methods are properly synchronized. Moreover, the programmer using the module may not be aware of its implementation and internal state, making it harder to judge what sequence of operations may cause atomicity violations.

1.3

Proposed Approach

To address this problem we propose a solution based on the design by contract method-ology. With our solution the developer of the module defines a contract that will clearly specify what might constitute an atomicity violation when using the module. This speci-fication stipulates which sequences of methods must be called in the same atomic scope to ensure that no atomicity violation will happen, preserving the correct behavior that is expected from the module.

The specification will act as a contract that the client code must respect for the module to have the expected behavior. The module’s code can be annotated with this specifica-tion, providing documentation on how to safely use the module.

We propose a static analysis to verify that the client complies with the module’s con-tract. This will ensure that the program is correct with respect to the module usage, or report the source of the violation if the contract is not met.

Consider again the example of the calendar module shown in Listing1(left). The con-tract of that module should say that calls toisFree()followed byschedule()should be

atomic as a whole. Listing1(right) shows the correct implementation ofschedule_event()

respecting this rule.

1.4

Contributions

The contributions of this dissertation can be summarized as:

• Definition of a specification to represent the contract of a module’s API. This

speci-fication clearly states what methods must be called in an atomic manner to preserve correctness.

(18)

respected, given the client code. Violations of the contract can easily be fixed with the information gathered by the analysis.

• Introduction of a novel approach to interprocedural control-flow static analysis,

based on context-free grammars. The context-free grammar captures the control flow structure of a program and parsing algorithms can then be used to verify control-flow-related properties.

• Implementation of a working prototype that apply the proposed analysis. Our

pro-totype analyses compiled Java programs and demonstrates the feasibility of our

analysis in real-world programming languages.

• Evaluation of the accuracy and efficiency of the proposed analysis.

1.5

Publications

An article describing the proposed solution was published and presented in INForum 2013 [SFL13]. A more in-depth paper is in preparation for future publication.

A publicly available repository with the implemented prototype can be found on https://github.com/trxsys/gluon. This repository also includes the evaluation tests used in Chapter5, and explained in AppendixA, as well as instructions on how to use the tool and to run the evaluation tests.

1.6

Outline

Chapter1introduced the subject addressed by this dissertation. It discusses the problem, its motivation, and the idea of our solution based on the design by contract programming methodology.

In Chapter 2 we present the related work of this thesis. We will cover the condi-tions for concurrent programs correctness and the common types of concurrency-related anomalies. We will also discuss proposed analysis techniques to detect these anomalies. This chapter will also address the design by contract methodology, and its application in subjects related with ours. This chapter will end with a view of the fundamentals of program analysis that will be useful to understand the analysis we propose.

Chapter3defines the methodology of our analysis. We will present the definition of a contract specification and the algorithm to verify the contract. This will be presented in an programming language-agnostic way, making the definition of the analysis more general. We also propose two extensions of the analysis to further improve the expressivity of a contract.

Chapter4discusses the implementation of our prototype, that was designed for the

Javaprogramming language. This chapter will deal with the prerequisites of the

(19)

1. INTRODUCTION 1.6. Outline

in that chapter, since they are specific to the programming language being analyzed. Chapter5evaluates our prototype, both validating the correctness of our analysis, and discussing performance results. The tests used in this evaluation are presented in Ap-pendixA.

(20)
(21)

2

Related Work

In this chapter we describe previous work related or useful to this thesis. This will contex-tualize how our contributions compare to the current state of the art of atomicity violation detection.

2.1

Concurrent Programs Correctness

Atomicity is a fundamental correctness property of concurrent systems. Intuitively a trace of a concurrent execution is said to be atomic if there is no interference between concurrent threads that lead to undesirable results. The notion of undesirable behavior varies from program to program, and the correctness criteria of a program may be tol-erant to weaker guarantees than others. To address different applications requirements several atomicity semantics were defined, which draw different trade-offs between the guarantees offered and performance. Sometimes a developer may choose a weaker atom-icity semantics for performance reasons, sacrificing correctness to some extent.

In this section we present the most important atomicity semantics.

Linearizability Linearizability [HW90] states that an atomic operation should appear

(22)

Threadt

o1 o2 o3

Threadt′

o′

1 o

2 o

′ 3

Figure 2.1: Example of linearizability of concurrent operations.

Threadt

o1 o2 o3

Threadt′

o′

1 o

2 o

′ 3

Figure 2.2: Example of serialization of concurrent operations.

Serializability The serializability semantics for atomic operations ensures that the state

of the program after the concurrent execution of the atomic operations is the same assome

sequential execution of those operations. This definition is more abstract than lineariz-ability since the state of the program is taken into account and therefore the semantics of the atomic operations are relevant. It is easy to see that every trace that is linearizable is also serializable.

It follows from the definition that the atomic operations can run in an arbitrary order, as exemplified in Figure2.2.

Strict Serializability In normal serializability we can freely choose a sequential

sce-nario that is equivalent to our trace. Strict Serializability is strictly stronger than serial-izability, enforcing the additional condition that if an atomic operation A that finishes before the beginning of the execution of operationBin the concurrent trace, thenAmust occur before operationB in the equivalent sequential execution. Figure2.3gives an ex-ample of a strict serialization of concurrent atomic operations. Dashed lines denote in-valid serialization points.

This atomicity model falls between serializability and linearizability in the correctness criteria hierarchy.

Snapshot Isolation Snapshot Isolation is the weakest atomicity semantic presented here.

(23)

2. RELATEDWORK 2.1. Concurrent Programs Correctness

Threadt

o1 o2 o3

Threadt′

o′

1 o

2 o

′ 3

Figure 2.3: Example of strict serialization of concurrent operations.

in the same memory cells. This allowswrite skewanomalies to happen [BBGMOO95]. A

write skew anomaly may occur when two transactions read overlapping memory regions that are then disjointly updated.

Even though this is a weak atomicity model, that allow well-known anomalies to happen, it is the most used model for database transactions since these anomalies are not common, and validating the transactions is made relatively cheap.

2.1.1 Race Conditions

Race conditions are anomalies that are caused by nondeterminism in a system, and lead to undesirable results. These anomalies were first identified in electronics, where the propagation time of a signal plus the reaction time of a logic gate introduce delays. There-fore, electric signals that theoretically arrive simultaneously drift by a small amount of time causing invalid outputs or an invalid state to be stored. One solution is to synchro-nizethe events of a electronic circuit with a clock that gives periodic ticks. These ticks are

points of synchronization where the whole state of the circuit is stable because enough time was given from the last tick to allow electric signals to fully propagate.

A similar definition applies to computer systems where different threads of execution run concurrently. The time of the execution of instructions of the multiple threads are not only arbitrary, but nondeterministic. This may lead to abnormalities if the threads share internal state. A solution to this is to impose synchronization between threads, reduc-ing the universe of the execution traces that are allowed to run. Many synchronization mechanisms were introduced, some more low-level, such as locks/mutexes, barriers and semaphores; and others more high-level such as monitors and transactional memory.

Many races conditions are caused by data races and atomicity violations [LPSZ08] which will be discussed next.

Data Races A data race happens when two threads concurrently access the same

mem-ory region and at least one access is a write [NM92]. This may lead to invalid results, e.g. reading values while they are being updated. Data races can be avoided by enforcing

(24)

Listing 2An example of a program containing a benign data race. 1 unsigned int new_views;

2 unsigned int total_views; 3

4 ... 5

6 void new_hit() 7 {

8 new_hits++;

9 atomic { total_hits++; } 10

11 if (new_hits > 1000)

12 {

13 atomic { log_hits(total_hits); } 14 new_hits=0;

15 }

16 }

It is worth noticing that a data race may not be a race condition. By definition a race condition implies some violation of the program correctness. While in general a data race leads to an incorrect behavior, this may not always be the case. This is exemplified in Listing2, where a web server maintains the number of hits on a certain HTTP resource. When 1000 new hits occur it logs the total of hits to keep a traffic history. The accesses to the shared variablenew_hitsare not protected and a data race can happen; however no

real harm can happen, even if this variable became corrupted, it only causes the log to be immediately written or postponed. This data races are calledbenignsince they occurrence

does not compromise the intended correctness of the program.

Atomicity Violation Atomicity violations are race conditions where the lack of

atomic-ity of the operations cause incorrect behavior of the program. This may be due to total lack of synchronization or to an incorrect scope of the synchronized blocks of code. The notion of atomicity violation is directly tied to the expected semantics of the program. Therefore an atomic violation is, by definition, a flaw in the program that causes incor-rect results or behavior.

A program can contain atomicity violations while being free of data races. This can happen when a program executes two operations in two atomic phases, and the intended behavior is only assured if these two operations execute as a single atomic operation.

2.1.2 Detecting Atomicity Violations

Atomicity violations are among the most common cause of errors in concurrent pro-grams [LPSZ08], and much research was done to try to identify this type of anomaly.

In this section we present some of the more relevant work in this area.

High-Level Data Races Artho et al defined the notion of view consistency in [AHB03].

(25)

2. RELATEDWORK 2.1. Concurrent Programs Correctness

Listing 3An example of a program containing a stale value error. 1 void withdraw(int v)

2 {

3 int current; 4

5 atomic { current=account.getBalance(); } 6 atomic { account.setBalance(current-v); } 7 }

of atomic operations in the code that should be atomic as a whole. A view of an atomic operationis the set of variables that are accessed in that atomic operation. The set of views

of a threadtis denoted as V(t), and a threadτ is said to be compatible with a view v if, and only if,{v ∩ v′

| v′

∈ V(τ)}forms a chain, i.e., is totally ordered under⊆. The

program is view consistent if every view from every thread is compatible with every other thread. The idea behind the definition of compatibility is that the viewvimplies a correlation between the variables it contains, and other threads should access these variables in a disciplined manner.

The notion of high-level data races (HLDR) does not capture every anomaly regarding the execution of atomic operations, and a HLDR does not imply a real atomicity violation. However this concept is precise enough to capture many real world anomalies.

This definition was subsequently extended by Praun and Gross [VPG04] to introduce

methods view consistency. A method view is the union of the views inside a method of a

class. The definition of method views consistency is analogous to the definition of view consistency. Furthermore this work distinguishes read and write accesses in the method views.

A further refinement of high-level data race was introduced by Dias et al in [DPL12]. This approach also takes into account the type of accesses in the views. They also re-fine the chain property for read accesses to reduce false positives: if there is no future dependency between the read variables of the two views the anomaly is not considered.

Stale Value Errors Stale value errors [BL04] are another type of anomalies that are also

related to multiple atomic operations that should be treated as a single atomic operation. These anomalies are characterized by reading a value in an atomic operation and reusing that value on subsequent atomic operations. This may represents an atomicity violation because the value may be outdated, since it could have been updated by a concurrent thread. The freshness of the values may or may not be a problem depending on the application.

Listing3show an example of a program with a stale value error. Here thewithdraw()

contains two atomic regions, and the valuecurrent, that is obtained in the first atomic

(26)

atomic blocks are hidden behind a function or method call.

Many analysis address, directly or indirectly, stale value errors [AHB04;BL04;DPL12;

LSTD11;FQ03;FF04;FFY08;VPG04;WS03]. We will briefly describe some of the analysis that directly target stale value errors. In [BL04] Burrows et al describe a dynamic analysis to detect stale value errors by instrumenting the program’s code to keeps track of stale variables. The program checks at run-time that every variable read is not stale, and aborts the execution otherwise. In [AHB04] Artho et al proposes a static analysis to detect stale values. This analysis statically infers the flow of data escaping synchronization regions and reports usages of those values in other synchronization blocks.

Access Patterns In [VTD06] Vaziri et al define eleven access patterns that potentially

represent an atomicity violation. These access patterns are sequences of read and write accesses denoted byRt(L)andWt(L)and represent, respectively, read and write accesses

to memory locationsLperformed by threadt. The sequence order represents the execu-tion order of the atomic operaexecu-tions. An example of an access pattern defined in Vaziri’s paper isRt(x) Wt′(x) Wt(x), that represents a stale value error, since threadtis

updat-ing variablexbased on an old value. The patterns make explicit use of the atomic set of variables, i.e., sets of variables that are correlated and must be accessed atomically. These correlated variables are assumed to be known. These eleven patterns are proved to be complete with respect to serializability.

A related approach by Teixeira et al [LSTD11] identifies three access patterns that capture a large number of anomalies. These anomalies are refereed toRwR, where two related reads are interleaved by a write in those variables;W rWwhere two related writes are interleaved by a read in those variables; andRwW that represents a stale value error.

Invariant Based Another approach to detect atomicity violations is by directly knowing

the intended semantics of the program. This was the approach followed by Demeyer and Vanhoof in [DV12]. The authors defined a pure functional concurrent programming lan-guage that is a subset ofHaskell, and includes theIO Monad, hence modeling sequential

execution and providing shared variables that can be accessed inside atomic transactions. A specification of the invariants of the program’s functions are provided by the program-mer in logic. A shared variable is said to be consistent if all invariants related to it hold before and after every atomic transaction. The static analysis acquires the facts about the program and feeds them to a theorem prover to test if every shared variable is consistent. This approach is very accurate provided that the programmer can express the no-tion of program correctness by using invariants on the global state, but is also expensive because a theorem prover is required to verify that the invariants hold.

Other Approaches Flanagan et al also proposed several methods for detecting

(27)

2. RELATEDWORK 2.1. Concurrent Programs Correctness

If a reduction exists from one trace to another then the execution of both traces yields the same state (although different states may be obtained intermediately). Reductions can be found by commuting right- and left-mover operations. Their analysis specifies which operations are movers and uses a result from Lipton’s Theory of Reductions, that speci-fied the conditions for a reduction to exists. If these conditions hold he shows that there is a reduction from the atomic operations in the dynamically obtained concurrent trace, to a serialization trace. If these conditions are not met, then an anomaly is reported. This can, however, lead to false positives.

Another publication from Flanagan et al provides a sound and completedynamic

anal-ysis for verifying serialization [FFY08]. This work uses a well-known result from database theory that states that a trace is serializable if and only if no cycle exists in the happens-before graph of the atomic operations [BHG87]. The dynamic analysis defined maintains this happens-before graph and report anomalies if a cycle is found.

A different approach is presented by Shacham et al in [SBASVY11]. In this work the atomic operations are extracted from the program to be analyzed to create an adversary that will run them concurrently to the original program. If two different runs yield dif-ferent results then an anomaly is reported. Some heuristics are used to explore the search space of possible interleavings from the adversary, avoiding a prohibitively expensive exhaustive search.

In [WZJ11] Weeratunge et al introduce an analysis to detect and fixHeisenbugs.

Heisen-bugs are race conditions that, due to non-determinism, are hard to reproduce. Their technique collects traces from the application and analyze consecutive pairs of accesses to shared memory. If some trace has a pair of accesses interleaved with some concurrent access to the variables involved, then that pair of accesses is considered erroneous and are locks are placed to protect them. A generic concurrency bug fixing strategy is pre-sented in [JZDLL12]. The detection of anomalies is given as a front-end to the fixer which then heuristically employ a fixing strategy.

(28)

the simulated execution of different scheduling; and the number of different scheduling of the atomic operations are exponential and may be unfeasible with a large number of atomic operations, even with pruning.

2.2

Design By Contract

Design by contract (DbC) is a software design technique to promote code reuse and soft-ware reliability, introduced by Meyer [Mey92]. The central notion of design by contract is that an object’s method should be treated as a service. In real world a service provider requires certain conditions in order for a service to be applicable. For example, a deliv-ery company requires the client to correctly provide the destination address, respect the maximum weight of packages, and, of course, pay. If these conditions are met by the client then the delivery company ensures the delivery of the package. Meyer suggests that these principles should also apply to object-oriented programming: a contract exists between the client of an object and that object. This contract specifies the preconditions the client code must meet in order for a method to be called and the postconditions that the method ensures after its execution. If the client calls the method without meeting the preconditions, no guarantee is offered about the correctness of the method’s result.

Design by contract is an alternative todefensive programming, where the programmer

assumes that a method may be called in any situation, with arbitrary arguments, and has to address all misuses that can happen. Listing4shows the functionsum(n), which

computes the sum of the firstnpositive natural numbers, in both defensive programming (left) and design by contract methodology (right). The DbC implementation provides simpler code and documents the admissible usage scenarios and function semantics in the contract. In an object the pre- and postconditions can also refer to the internal object’s state.

Listing 4Function programmed in defensive programming (left) and design by contract

(right).

1 int sum(int n) 2 {

3 if (n < 0)

4 // handle exception

5

6 return n*(n+1)/2; 7 }

1 @requires n≥0

2 @ensures returns Pni=0i

3 int sum(int n) 4 {

5 return n*(n+1)/2; 6 }

The main advantage of design by contract is the increase in the reliability and read-ability of the code. This methodology clearly describes the conditions and semantics for methods to be called. This provides documentation for developers, a way of identify-ing bugs, clearly assigns blame if some condition is not met, and allows code correctness verification.

(29)

2. RELATEDWORK 2.2. Design By Contract

assertions in run-time. This is the approach adopted by the Eiffel programming lan-guage [Mey87] where pre- and postconditions are syntactically part of the function def-inition. Other approaches use Hoare Logic [Hoa69] and theorem provers to statically verify that the code respects the contracts [FLLNSS02;BLS05].

The concept of design by contract methodology can be used to specify other types of contracts besides logic propositions over the program state. For instance, contracts can be used to describe access policies were certain methods are only allowed to be called by authenticated objects.

Concurrent Design by Contract Concurrency imposes a challenge to design by

con-tract methodology, since a client cannot always ensure that the preconditions are met before a method call: a concurrent execution might change the global state and make that assertion false. In [Mey97] Meyer presents a solution to this problem based on mon-itors. A second type of preconditions are introduced with a different semantics: when a method is called and the precondition does not hold, the caller blocks until the precondi-tion is satisfied. (We always assume mutual exclusion inside the object’s methods.) The postconditions are only guaranteed to hold immediately after the method execution.

Meyer’s solution may lead to deadlocks if the wait conditions are never met, thus failing to identify a bug. Nienaltowski et al propose a new contract semantics that applies both to sequential and concurrent programming [NMO09]: if the precondition depends only on the client, i.e., cannot be invalidated concurrently, then it must hold when the call is performed; otherwise the call is suspended until the precondition holds.

Contracts for Object Protocol Verification Some classes require that the sequence of

method calls must obey some restrictions. The set of legal sequences of calls to methods is called theclass protocol. As an example consider an object that represents a file: the first

method to be called must beopen(), then we can read or write to the file and we finish

by closing the file. A natural way of specifying this is by using an finite state automaton where each state allows a set of methods to be called, as exemplified in Figure2.4. Equiv-alently this may be represented by the regular expression(open(read|write)∗close)∗.

Beckman et al use typestate, a type system approach, to statically verify protocol com-pliance [BBA08]. The programmers define the object abstract states that correspond to the nodes in the finite state automaton. Each method requires the object to be in a defined abstract state and to make a transition to another state. In order for an object to be used it must first beunpackedto a specified abstract state, where only a subset of methods that

corresponds with that state are available. The type system statically detect if an unpack operation leads to an invalid abstract states.

(30)

closed opened open()

close()

read()/write()

Figure 2.4: Example of a protocol specification represented by an automaton.

its calls are a supersequence of the specification, whether the strong semantics requires an exact matching. The tool they implemented offers dynamic checking of the weaker semantics by instrumenting the method calls with assertions that check and maintain an finite state automaton associated with the called object.

Hurlin builds upon the previous work of Cheon, extending it to specify concurrent protocols [Hur09]. This extends the expressiveness of the protocol specification language by adding a conditional construct where the method that can be called depending on a boolean expression over the objects state; and a construct to specify that two methods can run concurrently. Analyzing the protocol compliance is done by adding JML-like assertions to the original program. The analysis can be performed statically by using a theorem prover to check the satisfiability of the assertions.

2.3

Program Analysis

Program analysis consists of detecting properties about a piece of software. The informa-tion obtained about a program is usually used to automatically detect and report possible flaws about that program, to obtain debug information that can help the programmer un-derstand the run-time behavior of the program, and to optimize the code of the program to achieve better run time or memory consumption.

This section will discuss the principal types and methodologies for program analysis, as well as the most common supporting data structures used to define an analysis.

2.3.1 Static and Dynamic Analysis

The two main approaches to program analysis are static and dynamic analysis. They differ in the way they gather information about the program and offer very distinct trade-offs, both in efficiency and precision.

(31)

2. RELATEDWORK 2.3. Program Analysis

instrumenting the program’s code is to run the program in a virtual machine that will keep track of the execution behavior of the program. Valgrind [NS03] is an example of a dynamic analysis tool that can be used to detect memory corruption and memory leak problems.

On the other hand, static analysis tries to infer the run-time behavior of the program by only analyzing its code. A static analysis takes as its input the source code or the compiled code of the program under analysis, and creates a model to represent the pro-gram. The representation will then be processed by the analysis algorithm to extract the relevant information about the program. These program representations will be further discussed in Section2.3.2. The type system of a statically typed language is an example of a static analysis.

When the goal of the analysis is to detect possible flaws in the program, the static analysis approach tends to be preferred. This is because it is possible for the static analysis to detectallthe flaws of the program1, but this often leads to an over-approximation of

the real anomalies of the program, reporting all the real flaws of the program and also

false positives, i.e., occurrences that the analysis mark as a flaw, but are in fact innocent.

Static analysis can be expensive to perform, depending on the complexity of the analysis. Dynamic analysis can be advantageous if a static version of the analysis is too expen-sive in terms of time or space, or if a static version reports many false positives. Since dynamic analysis only analyzes specific runs of a program, it only explores a limited set of the program’s states, which translate in a high number offalse negatives, i.e., real

flaws that are not reported. The trade-off is that the analysis has access to every run-time information that can lead to a high precision detection, which translates in a low or non-existent number offalse positives2.

Hybrid solutions exists, that perform static and dynamic analysis to programs. This types of analysis usually try to infer as much information as possible statically and com-plement this with a dynamic analysis to try to achieve the best of both worlds.

2.3.2 Program Representation

Static analysis verification tools usually offer different types of representation of the gram under analysis, with different levels of abstraction. These representations of pro-grams are used to extract the required information from simpler models of the program and avoid dealing with unnecessary information.

This section will cover some of the most commonly used representations of programs used in static analysis.

1An analysis that is guaranteed to detect all the flaws of a program is said to besound.

(32)

1 while (n >= 0) 2 {

3 s+=n*n;

4 n--;

5 }

while

>= sequence

n 0 +=

--s * n

n n

Figure 2.5: Example of an abstract syntax tree.

Abstract Syntax Tree The abstract syntax tree (AST) of a program represents the

syntac-tic structure of that program’s source code. This tree is generated by the source language parser, and gives a complete representation of the program. The nodes of the AST repre-sent syntactic constructions of the language, such asif,while,assignment,integer, etc. . .

This kind of structure is created directly from the source code, explicitly encoding the grammatical structure of the source programming language, getting rid of unnecessary syntactic details (henceabstractsyntax tree). Since it so closely represents the source code

it is usually used to generate other data structures that are more suitable for program analysis.

Figure2.5exemplifies a abstract syntax tree (right) of a simple block of code (left). As shown the abstract syntax tree simply encodes the grammatical structure of the program.

Intermediate Representation An intermediate instruction representation is a language

that represents the semantics of the program with simple instructions. This is benefi-cial for program optimization and analysis, because it breaks down a complex high-level language to a reduced set of instructions, which can more easily be processed by a static analysis algorithm. Another advantage is that an intermediate representation can be gen-erated by different front-ends that process different high-level languages, making the analysis more generic.

A common type of intermediate languages is thethree address code, where each

instruc-tion has, at most, three operands. This instrucinstruc-tions can accept operands with constant values and memory addresses. These memory addresses do not necessarily correlate with concrete memory addresses, and can seen as registers in a virtual processor, that are latter translated in concrete register or memory addresses by the back-end of the compiler that generates the target machine’s code. Another variation of intermediate representa-tion is thestatic single assignment form (SSA) [RWZ88] where every variable is assigned

(33)

2. RELATEDWORK 2.3. Program Analysis

Listing 5Example of three address code.

1 for (i=0; i < 10; i++) 2 s=s+i*i;

1 i = 0

2 L: b = i >= 10 3 if b goto E 4 t = i*i 5 s = s+t 6 i = i+1

7 goto L

8 E:

1 while (n != 1) 2 {

3 if (n%2 == 0)

4 n=n/2;

5 else

6 n=3*n+1;

7 c++;

8 }

entry

n != 1

n%2 == 0

n=n/2 n=3*n+1

c++

exit

Figure 2.6: Example of a control flow graph.

Control Flow Graph A control flow graph [All70] of a method or function captures

the control flow paths the method can take. In this graph the vertices represent single instructions of the program. An edgeu→vrepresents that instructionucan be immedi-atly followed by the execution of instructionv. Therefore the successors of an instruction iin the control flow graph represent the different alternatives the program may take in run-time after executingi(unless the execution ofibreaks the regular control flow, for instance, by raising an exception or terminating the program).

Figure2.6exemplifies a control flow graph (right) of a simple block of code (left). As can be seen, the control flow graph encompasses all the control flow logic of the program.

Call Graph A call graph [Ryd79] represents the call relations between methods or

func-tions of a program. This is represented in a directed graph where the vertices are meth-ods/functions, and an edgeu → v exists if, and only if, the methodu can directly calls methodv. The graph contains no information about the order by which the methods are called nor the number of times the method is invoked.

(34)

1 boolean even(int n) 2 {

3 if (n == 0) return true; 4 else return odd(n-1); 5 }

6

7 boolean odd(int n) 8 {

9 if (n == 0) return false; 10 else return even(n-1); 11 }

12

13 void g(int n) { return n*n; } 14

15 void f(int n) 16 {

17 if (odd(n)) return n+1; 18 else return g(n); 19 }

odd even

f g

Figure 2.7: Example of a call graph.

main method. It is also easy to detect direct and indirect recursion since they form loops in the graph. Figure2.7shows an example of a call graph (right) and the corresponding methods (left).

2.3.3 Types of Static Code Analysis

Many static analysis are follow well-studied general methodologies for analysis design. The most common of these frameworks for analysis will be presented in this section.

Data Flow Analysis Data flow analysis [Coc70;All70;KU77] works directly on the

con-trol flow graph of a method. Each node of the concon-trol flow graph will have a value asso-ciated. The concrete analysis defines which types of values are associated with the nodes of the control flow graph, and how to propagate these values to the neighbor nodes. Each type of node can have specific rules on how to manipulate the values received from the adjacent nodes (forming a set of constraints relating the values of the nodes). A fixed-point algorithm is then used to propagate all information across the control flow graph until the values stabilize. (To ensure that a fixed-point is always reached the values as-sociated with the nodes of the control flow graph must form a finite-height lattice over the constraints defined.) The final result of the analysis is the values associated with the nodes after the fixed-point is reached. If the analysis is defined in a conservative manner the results will always be sound.

Constraint Based Analysis Analysis based on Constraints [Aik94] are composed by

(35)

2. RELATEDWORK 2.3. Program Analysis

constrains can be equation that express relations between variables. After the generation of set of constrains they must be resolved to obtain the desired end result of the analysis.

Abstract Interpretation Abstract interpretation [CC77] is a technique used to acquire

information about the semantics of a program. The analysis defines anabstract semantics

of the target programming language. This abstract semantics over-approximates the con-crete semantics of the program, in order to avoid false positives, i.e., it considers all pos-sible concrete execution of the program. The abstract semantic is defined in such a way that it always terminates, and computes the information required about the program.

As an example, say we want to know all the variables that are modified in a block of code. The abstract semantics for that analysis will record the variables written in every assignment, and simply propagate that set of written variables along the program. It will explore all alternatives of the conditional statements and will “iterate” every loop exactly once. The end result would be a superset of the variables accessed in every possible concrete execution of that block of code.

Type and Effect Systems Type systems [Car96] are the type of static analysis a

pro-grammer most often deals with. It assigns types (which can be seen as set of values) to syntactic expressions based on well-defined inference rules. If no rule applies to a pro-gram’s expression the program is not well-typed and is rejected by the type system. The most common use of a type system is to avoid run-time errors by making sure that the values of expressions are acceptable in the context being used.

More sophisticated type systems may annotate the types of expressions with more information as to detect more complex errors.

Effect systems extends type systems with annotations about the effects that expression make produce. Examples of effects that can be described by effect systems are changing global variables, performing input/output, allocating memory, etc. . . An example of an effect system is theJavachecked exceptions, which make sure that a method handles the

(36)
(37)

3

Methodology

This chapter presents the static analysis for the verification of the module’s contract. This analysis is the main contribution of this dissertation. We will define what constitutes a contract of the module and its semantics. The analysis has two fundamental phases: the extraction of the behavior of the program, and the verification of the module’s contract based on the extracted behavior.

We also propose two extensions to the analysis that augment the expressivity of the contract, allowing a developer to specify in more detail what may lead to a atomicity violation.

3.1

Analysis Overview

The analysis we propose verifies statically if a client program complies with the contract of a given module. This is achieved by verifying that the threads launched by the pro-gram always execute atomically the sequence of calls defined by the contract.

This analysis has the following phases:

i) Determine the entry methods of the threads the program may launch.

ii) Determine which of the program’s methods are atomically executed. A method is

atomically executed if it is atomic1 or if the method is always called by atomically

executed methods.

(38)

iii) Extract the behavior of each of the program’s threads with respect to the usage of the module under analysis.

iv) For each thread, check that its usage of the module respects the contract as defined in Section3.2.

For the analysis to be applicable we require the following conditions: a) it must be possible to identify when the module is used in the target programming language; b) the accesses to the module are always done through a well defined API; c) it must be possible to identify the regions of code that run atomically; d) it must be possible to identify the starting point of the threads the program may launch.

In Section 3.2 we define the contract of the module. Section 3.3 we introduce the algorithm that extracts the program’s behavior with respect to the module’s usage. Sec-tion3.4 defines the methodology that verifies whether the extracted behavior complies to the contract.

3.2

Contract Specification

The contract of a module specifies which sequences of calls of its methods must be exe-cuted atomically, as to avoid atomicity violations in the module’s client program. In the spirit of thedesign by contractmethodology, we assume that the definition of the contract,

including the identification of which sequences of calls should be executed atomically, is a responsibility of the module’s developer.

Definition 1(Contract). The contract of a module with public methodsm1,· · ·, mnis of

the form,

1. e1

2. e2 ... k. ek

where each clausei = 1,· · ·, kis described byei, a star-free regular expression over the

alphabet{m1,· · ·, mn}. Star-free regular expressions are regular expressions without the

Kleene star, using only the alternative (|) and the concatenation (implicit) operators.

Each sequence defined inei must be executed atomically by the program using the

module, otherwise there is a violation of the contract. The contract specifies a finite num-ber of sequences of calls, since it is the union of star-free languages. Therefore, it is pos-sible to have the same expressivity by explicitly enumerating all sequences of calls, i.e., without the use of the alternative operator. We chose to offer the alternative operator so that the programmer can group similar scenarios under the same clause.

(39)

3. METHODOLOGY 3.3. Extracting the Behavior of a Program

a method to simulate this operator, maintaining a finite number of call sequences, at the expense of precision loss.

Example Consider the classjava.util.ArrayListthat represents arrays, offered by the

Java standard library. For simplicity we will only consider a few methods: add(obj), get(idx),set(idx, obj),contains(obj),indexOf(obj),remove(idx), andsize().

The following contract defines some of the clauses for this class.

1.contains indexOf

2.indexOf(remove|set|get) 3.size(remove|set|get) 4.add indexOf.

Clause 1 of ArrayList’s contract denotes that the execution of contains()followed

byindexOf()should be atomic, otherwise the client program may confirm the existence

of an object in the array, but fail to obtain its index due to a concurrent modification. Clause2represents a similar scenario where, the position of the obtained object is

modi-fied. In clause3we deal with the common situation where the program verifies if a given index is valid before accessing the array. To make sure that the size obtained bysize()is

valid when accessing the array we should execute these calls atomically. Clause4

repre-sents scenarios where an object is added to the array and then the program tries to obtain information about that object by querying the array.

Another relevant clause is contains indexOf (remove | set | get), but the contract’s

semantic already enforces the atomicity of this clause as a consequence of the composition of clauses1and2, as they overlap in theindexOf()method.

3.3

Extracting the Behavior of a Program

The behavior of the program with respect to the module usage can be modeled as the individual behavior of all the threads the program may launch. The usage of a module by a threadtof a program can be described by a languageLover the alphabetm1,· · ·, mn,

the public methods of the module. A wordm1· · · mn∈Lif some execution oftmay run

the sequence of callsm1,· · ·, mnto the module.

To extract the usage of a module by a client program, our analysis generates a context-free grammar that represents the languageLof a threadtof the client program, which is represented by its control flow graph (CFG). The CFG of the threadtrepresents every possible path that the control flow may take during its execution. In other words, the analysis generates a grammarGtsuch that, if there is an execution path oftthat runs the

sequence of callsm1,· · ·, mn, then m1· · ·mn ∈ L(Gt). (The language represented by a

(40)

Definition 2(Thread Behavior Grammar). The grammarGt= (N,Σ, P, S), is built from

the CFG of the client’s program threadt. We define,

• N, the set of non-terminals, as the set of nodes of the CFG. Additionally we add non-terminals that represent each method of the client’s program (represented in calligraphic font);

• Σ, the set of terminals, as the set of identifiers of the public methods of the module under analysis (represented in bold);

• P, the set of productions, as described bellow, by rules3.1–3.5;

• S, the grammar initial symbol, as the non-terminal that represents the entry method of the threadt.

For each methodf()that threadtmay run we add toP the productions that respect the rules3.1–3.5. Method f()is represented byF. A CFG node is denoted byα : JvK,

whereα is the non-terminal that represents the node andvits type. We distinguish the following types of nodes: entry, the entry node of methodF;mod.h(), a call to methodh()

of the modulemodunder analysis;g(), a call to another methodg()of the client program;

andreturn, the return point of methodF. Thesucc:N → P(N)function is used to obtain the successors of a given CFG node.

ifα:JentryK, {F →α} ∪ {α→β |β ∈succ(α)} ⊂P (3.1) ifα:Jmod.h()K, {α→hβ|β ∈succ(α)} ⊂P (3.2) ifα:Jg()K, {α→ Gβ |β∈succ(α)} ⊂P whereGrepresentsg() (3.3)

ifα:JreturnK, {α→ǫ} ⊂P (3.4)

ifα:JotherwiseK, {α→β |β ∈succ(α)} ⊂P (3.5)

No more productions belong toP.

The rules3.1–3.5capture the structure of the CFG in the form of a context-free gram-mar. Intuitively this grammar represents the flow control of the threadtof the program, ignoring everything that is not related with the module’s usage. The grammar is built in such a way that iff g ∈ L(Gt)then the threadtmay invoke methodmod.f(), followed

bymod.g().

Rules3.1–3.5 preserve the structure of the control flow graph, so that every path in the graph corresponds to a derivation in the grammar. Rule3.1adds a production that re-lates the non-terminalF, that represents methodf(), to the entry node of the CFG off().

This will allow other productions that reference methodf()to use the non-terminal F.

(41)

3. METHODOLOGY 3.3. Extracting the Behavior of a Program

1 void f() 2 {

3 m.a(); 4 if (cond)

5 g();

6 m.b(); 7 }

8

9 void g() 10 {

11 m.c(); 12 if (cond)

13 { 14 g(); 15 m.d(); 16 f(); 17 } 18 } f() g() entry m.a() cond g() m.b() return A B C D E F entry m.c() cond g() m.d() f() return G H I J K L M

Figure 3.1: Program with recursive calls using the module m(left) and respective CFG

(right).

If a CFG nodeA callsmod.h()and has a successorB then the productionA → hB will be in the grammar, and can be read as “non-terminalAgenerates all words ofBprefixed withh”. Rule3.3handles calls to another methodg()of the client program (methodg()

will have its non-terminalG added by Rule 3.1). The return point of a method simply

adds anǫproduction to the grammar (Rule3.4). All others types of CFG nodes are han-dled uniformly, preserving the CFG structure by making them reducible to the successor non-terminals (Rule 3.5). It is important to notice that only the client program code is analyzed.

The Gt grammar may be ambiguous, i.e., offer several different derivations to the

same word. Each ambiguity in the parsing of a sequence of calls m1· · · mn ∈ L(Gt)

represents different contexts where these calls can be executed by threadt. It is, therefore, necessary to allow such ambiguities so that the verification of the contract can cover all the occurrences of the sequences of calls in the client program.

The languageL(Gt) contains every sequence of calls the program may execute, i.e.,

it produces no false negatives. HoweverL(Gt) may contain sequences of calls that the

program does not execute (for instance calls performed inside a block of code that is never executed), which may lead to false positives.

Examples Figure3.1(left) shows a program that consists of two methods that call each

other mutually. We assume that methodf()is the entry point of the thread. The module

under analysis is represented by object m. The control flow graphs of these methods

(42)

G1 = (N1,Σ1, P1, S1), where

N1={F,G, A, B, C, D, E, F, G, H, I, J, K, L, M},

Σ1={a,b,c,d},

S1=F,

andP1has the following productions:

F →A G →G

A→B H→cI

B→aC I →J |M

C→D|E J → GK

D→ GE K→dL

E→bF L→ FM

F →ǫ M →ǫ

In this example we can see how the grammar mimics the control flow graph struc-ture. For each edgeu → v of the CFG the grammar includes a production of the form u→ · · · v, which captures the control flow of the program as a grammar.

Context-free grammars can easily represent loops and recursion of the code. This example show how a recursive call is encoded in the grammar: since methods are repre-sented by non-terminals (which we show in a calligraphic font) we can use them in the body of any production. The example above shows this, for instance, in the production J → GKthat represents a direct recursive call tog().

A second example, shown in Figure3.2, exemplifies how the Definition2handles a flow control with loops. In this example we have a single functionf(), which is assumed

to be the entry point of the thread. For this example we haveG2= (N2,Σ2, P2, S2), with

N2 ={F, A, B, C, D, E, F, G, H},

Σ2 ={a,b,c,d},

S2 =F.

The set of productionsP2is,

F →A E →cF

A→B F →B

B →aC |aG G→dH

C →D|E H→ǫ

(43)

3. METHODOLOGY 3.4. Contract Verification

1 void f() 2 {

3 while (m.a())

4 {

5 if (cond)

6 m.b(); 7 else 8 m.c(); 9 10 count++; 11 } 12 13 m.d(); 14 } entry m.a() cond m.b() m.c() count++ m.d() return A B C D E F G H

Figure 3.2: Program using the modulem(left) and respective CFG (right).

To better understand how the grammar deals with loops we will show a derivation of the worda b a c a d. The only way for the program to perform these calls to modulemis

to iterate the loop exactly twice, entering the “then” branch of the if in the first iteration and the “else” branch in the second.

The derivation ofa b a c a dis unambiguously done as follows:

F ⇒A⇒B⇒aC⇒aD⇒a bF ⇒a bB ⇒a b aC ⇒a b aE

⇒a b a cF ⇒a b a cB ⇒a b a c aG⇒a b a c a dH⇒a b a c a d.

3.4

Contract Verification

The verification of a contract must ensure that all sequences of calls specified by the con-tract are executed atomically by all threads the client program may launch. Since there is a finite number of call sequences defined by the contract we can verify each of these sequences to check if the contract is respected.

Algorithm1presents the pseudo-code of the algorithm that verifies a contract against a client’s program. For each threadtof a programP, it is necessary to determine if (and where) any of the sequences of calls defined by the contractw =m1,· · ·, mnoccur inP

(line 4). To do so, each of these sequences are parsed by the grammar G′

t (line 5), the

grammar includes all words and sub-words ofGt. Sub-words must be included since we

want to take into account partial traces of the execution of threadt. Notice thatG′

tmay be ambiguous. Each different parsing tree represents different

lo-cations where the sequence of callsm1,· · ·, mnmay occur in threadt. Functionparse()

(44)

Algorithm 1Contract verification algorithm. Require: P, client’s program;

C, module contract (set of allowed sequences). 1: fort∈threads(P)do

2: Gt←build_grammar(t)

3: G′

t←subword_grammar(Gt)

4: forw∈Cdo 5: T ←parse(G′

t, w)

6: forτ ∈T do

7: N ←lowest_common_ancestor(τ, w) 8: if¬run_atomically(N)then

9: return ERROR 10: return OK

location of each method call ofm1,· · ·, mn in programP (since non-terminals represent

CFG nodes). Additionally, by going upwards in the parsing tree, we can find the node that represents the method under which all calls tom1,· · ·, mnare performed. This node

is the lowest common ancestor of terminalsm1,· · ·, mnin the parsing tree (line 7).

There-fore we have to check that the obtained lowest common ancestor is always executed atomically (line 8) to make sure that the whole sequence of calls is executed under the same atomic context. Since it is thelowestcommon ancestor we are sure to require

min-imal synchronization from the program. A parsing tree contains information about the location in the program where a contract violation may occur, therefore we can offer de-tailed instructions to the programmer on where this violation occurs and how to fix it.

GrammarGtcan use all the expressiveness offered by context-free languages. For this

reason it is not sufficient to use the LR(·) parsing algorithm [Knu65], since it does not handle ambiguous grammars. To deal with the full class of context-free languages a GLR parser (GeneralizedLRparser) must be used. GLR parsers explores all the ambiguities that can generate different derivation trees for a word. We distinguish three parsing algorithms that offer good worst-case time complexity and are therefore suitable to be used: Tomita [Tom87], CYK [You67], and Earley [Ear70].

Another important point is that the number of parsing trees may be infinite. This is due to loops in the grammar, i.e., derivations from a non-terminal to itself (A ⇒ · · · ⇒ A). An infinite number of parsing trees may occur in a loop in the grammar that is not productive to parse the grammar, and the parsing algorithm can choose to iterate that loop an arbitrary number of times, creating one parsing branch for each possibility. This often occur in Gt(every loop in the control flow graph will yield a corresponding loop

in the grammar). For this reason the parse()function must detect and prune parsing

(45)

3. METHODOLOGY 3.4. Contract Verification

1 void atomic run() 2 {

3 f();

4 m.c(); 5 }

6

7 void f() 8 {

9 m.a();

10 g();

11 } 12

13 void g() 14 {

15 while (cond)

16 m.b();

17 }

R → F c

F →aG G →A A→B |ǫ B →bA

a b b c

B A A ǫ G A B F R

Figure 3.3: Program (left), simplified grammar (center) and parsing tree ofa b b c(right).

Examples Figure3.3shows a program (left), that uses the modulem. The methodrun()

is the entry point of the threadt and is atomic. In the center of the figure we show a simplified version of theGt grammar. (The G′t grammar is not shown for the sake of

brevity.) The methods run(), f(), andg() are represented in the grammar by the

non-terminals R, F, andG respectively. If we apply Algorithm 1to this program with the

contractC = {a b b c}the resulting parsing tree, denoted byτ (line6 of Algorithm1), is represented in Figure 3.3 (right). To verify that all calls represented in this tree are executed atomically, the algorithm determines the lowest common ancestor ofa b b cin

the parsing tree (line7), in this exampleR. SinceRis always executed atomically (atomic keyword), it is ensured that the contract of the module is respected by the client program. Figure 3.4 exemplifies a situation where the generated grammar is ambiguous. In this case the contract is C = {a b}. The figure shows the two distinct ways to parse

the word a b (right). Both these trees will be obtained by our verification algorithm

(line 5 of Algorithm1). The first tree (top) hasF as the lowest common ancestor ofa b.

SinceF corresponds to the methodf(), which is executed atomically, this tree respects

the contract. On the other hand, the second tree (bottom) hasR as thelowest common ancestorofa b, corresponding to the execution of theelsebranch of methodrun(). This

non-terminal (R) does not correspond to an atomically executed method, therefore the

contract is not met and a contract violation is detected.

Sub-word Grammar To create a grammar with every sub-word ofGwe can employ the following conversion. LetG= (N,Σ, P, S)be a context-free grammar. Assume, without

loss of generality, thatGis in Chomsky normal form2 and does not generate the empty language. This means that every production of the grammar is of the formA → B C or A → α, where A, B, C are non-terminals and α is a terminal. We want to define a

(46)

1 void run() 2 {

3 if (...)

4 f(); 5 else 6 { 7 m.a(); 8 g(); 9 } 10 } 11

12 void atomic f() 13 {

14 m.a();

15 g();

16 } 17

18 void atomic g() 19 {

20 m.b(); 21 }

R →aG | F F →aG

G →b

a b G R F a b G R

Figure 3.4: Program (left), simplified grammar (center) and parsing trees ofa b(right).

grammarG′

= (N′

,Σ, P′

, S′

)such thatG′ generates all sub-word of

Gand no more:

L(G′

) ={w| ∃ω, ω′

ωwω′

∈ L(G)}.

We defineG′

= (N′

,Σ, P′

, S′

)as,

A∈N ⇔A, A<, A>, A<>∈N′

S′

=S<>

A→B C∈P ⇔A→B C ∈P′

∧A< →B<|B C<∈P′

∧A> →C>|B>C∈P′

∧A<> →B>C<|B<>|C<>∈P′

A→α∈P ⇔A→α∈P′

∧A< →ǫ|α∈P′

∧A> →ǫ|α∈P′

∧A<> →ǫ|α∈P′

.

For each non-terminal we add three new non-terminals:A<,A>, andA<>. Intuitively

these non-terminals represents, respectively, all prefixes, suffixes and sub-words ofA. For example the productionA→B CinGgenerates the productionsA<>→B>C<|B<>|C<> inG′, which can be read as “the sub-words of

Imagem

Figure 1.1: Example of concurrent thread scheduling leading to an atomicity violation.
Figure 2.1: Example of linearizability of concurrent operations.
Figure 2.3: Example of strict serialization of concurrent operations.
Figure 2.4: Example of a protocol specification represented by an automaton.
+7

Referências

Documentos relacionados

The probability of attending school four our group of interest in this region increased by 6.5 percentage points after the expansion of the Bolsa Família program in 2007 and

social assistance. The protection of jobs within some enterprises, cooperatives, forms of economical associations, constitute an efficient social policy, totally different from

Abstract: As in ancient architecture of Greece and Rome there was an interconnection between picturesque and monumental forms of arts, in antique period in the architecture

We also determined the critical strain rate (CSR), understood as the tangent of the inclination angle between the tangent to the crack development curve and the crack development

The iterative methods: Jacobi, Gauss-Seidel and SOR methods were incorporated into the acceleration scheme (Chebyshev extrapolation, Residual smoothing, Accelerated

Ao Dr Oliver Duenisch pelos contatos feitos e orientação de língua estrangeira Ao Dr Agenor Maccari pela ajuda na viabilização da área do experimento de campo Ao Dr Rudi Arno

This log must identify the roles of any sub-investigator and the person(s) who will be delegated other study- related tasks; such as CRF/EDC entry. Any changes to

Ao Colegiado do Curso de Licenciatura em Educação do Campo da Universidade Federal do Maranhão, pela liberação do meu afastamento das atividades docentes para a conclusão do