interpretation [ 11,12] is a form of program analysis that maps programs into more abstract domains. This makes analysis more tractable and potentially useful for checking. The technique requires safety and completeness, however; analysis must be correct for all possible inputs. It also has difficulty in practice with large programs. In contrast, error checking, to be of practical value, must be able to handle large programs. Furthermore, error messages need not always be correct as long as the number and type of spurious ones are below usability thresholds. In contrast to the above techniques, the debugging tool Purify [ 13] and similar runtime memory debuggers detect a broad range of errors and require no extra programmer effort to use. They are, however, debuggers, operating on heavily instrumented executables (see, for example, [ 14]) and requiring test cases, which impose serious limitations. Thus, the goal of the research reported here was to develop a source code analyzer that could find Purify-like errors with Purify’s ease of use, but without needing test cases. This goal led to a few specific requirements. • Real world programs written in C and C++ should be checked effectively . Analysis must therefore handle such difficulties as pointers, arrays, aliasing, structs and unions, bit field operations, global and static variables, loops, gotos, third party libraries, recursive and mutuallyrecursive functions, pointer arithmetic, arbitrary casting (including between pointer and integer types), and overloaded operators and templates (for C++). Copyright 2000 John Wiley & Sons, Ltd. Softw. Pract. Exper. 2000;30:775–802 FINDING DYNAMIC PROGRAMMING ERRORS 777 • Information should be derived from the program text rather than acquired through user annotations. This is possible because the semantics of a language imply certain consistency rules, and violations of these rules can be identified as defects. For example, the semantics of local variables allow for the detection of defects such as using uninitialized memory. • Analysis should be limited to achievable paths; that is, sequences of program execution which can actually occur in practice . This requires detailed tracking of actual values, not just performing dataand control-flow analysis. • The information produced from the analysis should be enough to allow a user to characterize the underlying defects easily . This is especially important, and hard to achieve, with large programs. In response to these goals, a new method of analysis was developed, based on simulating the execution of individual functions. The method can be summarized in a few key concepts. • Simulation specifically consists of sequentially tracing distinct execution paths through the function being analyzed, and simulating the action of each operator and function call on the path on an underlying virtual machine. By tracking the state of memory during path execution, and applying the consistency rules of the language to each operation, inconsistencies can be detected and reported. In addition, by examining the current state of memory whenever a conditional is encountered, the analysis can be restricted to achievable paths. Because of the detailed tracking of paths and values, precise information is available to help the user understand the situation in which the defect manifests itself. • The behavior of a function is described as a set of conditionals, consistency rules and expression evaluations. This summary of the behavior is called a model of the function. Whenever a function call is encountered during path execution, the model for that function is used to determine which operations to apply. • The information produced while simulating a function is sufficient to generate a model for that function automatically. • To apply these techniques to an entire program, or subset of a program, analysis begins with the leaf functions of the call graph and proceeds bottom-up to the root. As each function in turn is simulated, defects are identified and reported, and the model for that function is available for subsequent simulation of its callers. • This bottom-up approach uses a function’s implementation to generate constraints on the callers of that function. This is particularly valuable in situations where the text of the complete program is not available, either because the program is only partially implemented, or because the code under analysis is designed as a component that may fit into many different programs. An error detection tool for C and C++, called PREfix, was built based on these techniques. It has been used on several large commercial programs. The remainder of this paper discusses in detail the operation of PREfix and presents some experience with it.
[1]
Karl N. Levitt,et al.
SELECT—a formal system for testing and debugging programs by symbolic execution
,
1975
.
[2]
Murray Hill,et al.
Lint, a C Program Checker
,
1978
.
[3]
Chris Hankin,et al.
Abstract Interpretation of Declarative Languages
,
1987
.
[4]
Todd M. Austin,et al.
Efficient detection of all pointer and array access errors
,
1994,
PLDI '94.
[5]
Chuck Allison,et al.
The standard C library, part 3
,
1995
.
[6]
Monica S. Lam,et al.
Efficient context-sensitive pointer analysis for C programs
,
1995,
PLDI '95.
[7]
Daniel Jackson.
Aspect: detecting bugs with abstract dependences
,
1995,
TSEM.
[8]
Flemming Nielson,et al.
Abstract interpretation: a semantics-based tool for program analysis
,
1995,
LICS 1995.
[9]
David E. Evans,et al.
Static detection of dynamic memory errors
,
1996,
PLDI '96.
[10]
Nils Klarlund,et al.
Automatic verification of pointer programs using monadic second-order logic
,
1997,
PLDI '97.
[11]
K. Rustan M. Leino,et al.
Extended static checking
,
1998,
PROCOMET.
[12]
Michael Rodeh,et al.
Detecting memory errors via static pointer analysis
,
1998
.