Program Slicing in the Presence of Pointers

Similar documents
Program Slicing in the Presence of Pointers (Extended Abstract)

Class 6. Review; questions Assign (see Schedule for links) Slicing overview (cont d) Problem Set 3: due 9/8/09. Program Slicing

An Efficient Algorithm for Computing all Program Static Slices

A classic tool: slicing. CSE503: Software Engineering. Slicing, dicing, chopping. Basic ideas. Weiser s approach. Example

Barrier Slicing and Chopping

Conditioned Slicing. David Jong-hoon An. May 23, Abstract

Identifying Parallelism in Construction Operations of Cyclic Pointer-Linked Data Structures 1

Precise Executable Interprocedural Slices

Advanced Slicing of Sequential and Concurrent Programs

Static Slicing in the Presence of GOTO Statements. IBM T. J. Watson Research Center. P. O. Box 704. Yorktown Heights, NY

Topics in Program Slicing

Efficient Program Slicing Algorithms for Measuring Functional Cohesion and Parallelism

The Relationship between Slices and Module Cohesion

Inter-procedural static slicing using advanced caching algorithm

An Analysis of the Current Program Slicing and Algorithmic Debugging Based Techniques. JOSEP SILVA Technical University of Valencia

Inter-Procedural Static Slicing Using Advanced Caching Algorithm *

An approach to recovering data flow oriented design of a software system

A Run-Time Type-Checking Debugger for C

A Slicing Method for Object-Oriented Programs Using Lightweight Dynamic Information

Unit 7. Functions. Need of User Defined Functions

Slicing, Chopping, and Path Conditions with Barriers

Bisection Debugging. 1 Introduction. Thomas Gross. Carnegie Mellon University. Preliminary version

Analysis of Pointers and Structures

Interprocedural Slicing Using Dependence Graphs

Slicing, I/O and the Implicit State. Project Project, Eden Grove, London, N7 8DB. tel: +44 (0) fax: +44 (0)

Data and File Structures Laboratory

A Vocabulary of Program Slicing-Based Techniques

Building Executable Union Slices using Conditioned Slicing

[16] Ottenstein, K and Ottenstein, L The Program Dependence Graph in Software Development

cies. IEEE Trans. Comput., 38, 5 (May),

Weeks 6&7: Procedures and Parameter Passing

Lecture 14 Pointer Analysis

Why arrays? To group distinct variables of the same type under a single name.

Control-Flow Analysis

CSCI 171 Chapter Outlines

Interprocedural Constant Propagation using Dependence Graphs and a Data-Flow Model

Dynamic Slicing in the Presence of Unconstrained Pointers. Technical Report SERC-TR-93-P. Hiralal Agrawal. Richard A. DeMillo

DETERMINE COHESION AND COUPLING FOR CLASS DIAGRAM THROUGH SLICING TECHNIQUES

Using Program Slicing. in Software Maintenance. J. R. Lyle Wilkens Avenue. Abstract

Dependence-Cache Slicing: A Program Slicing Method Using Lightweight Dynamic Information

Review of the C Programming Language

Declaring Pointers. Declaration of pointers <type> *variable <type> *variable = initial-value Examples:

Increasing the Accuracy of Shape and Safety Analysis of Pointer-based Codes

Review of the C Programming Language for Principles of Operating Systems

Points-to Analysis. Xiaokang Qiu Purdue University. November 16, ECE 468 Adapted from Kulkarni 2012

Regression Testing for Trusted Database Applications

Types-2. Type Equivalence

Qualifying Exam in Programming Languages and Compilers

Program Analysis. Readings

Guernsey Post 2013/14. Quality of Service Report

Eindhoven University of Technology MASTER. Program slicing for Java 6 SE. de Jong, J.P. Award date: Link to publication

Static Analysis of Accessed Regions in Recursive Data Structures

Program Slicing David W. Binkley Keith Brian Gallagher Ahundred times every day, I remind myself that my inner and outer life depends on the labors of

Source Code Analysis and Slicing for Program Comprehension

Efficient Debugging with Slicing and Backtracking

Institut für Informatik D Augsburg

Lecture 16 Pointer Analysis

CodeSurfer/x86 A Platform for Analyzing x86 Executables

CS61 Section Notes. Section 5 (Fall 2011) Topics to be covered Common Memory Errors Dynamic Memory Allocation Assignment 3: Malloc

Lecture Notes on Common Subexpression Elimination

Types. What is a type?

Actually, C provides another type of variable which allows us to do just that. These are called dynamic variables.

More Dataflow Analysis

Union Slices for Program Maintenance

Lecture 27. Pros and Cons of Pointers. Basics Design Options Pointer Analysis Algorithms Pointer Analysis Using BDDs Probabilistic Pointer Analysis

Lecture 20 Pointer Analysis

A Beginner s Guide to Programming Logic, Introductory. Chapter 6 Arrays

Debugging Program Slicing

Short Notes of CS201

Lecture Notes on Garbage Collection

Dependence Graph Considerering Resource for Automatic Sample Program Generation

Program Slicing. Keith Gallagher Computer Science Department University of Durham South Road Durham DH1 3LE, UK

A Fast Review of C Essentials Part I

Static Slicing of Threaded Programs

CS201 - Introduction to Programming Glossary By

Support for Automatic Refactoring of Business Logic

C Structures, Unions, Bit Manipulations, and Enumerations

Memory and Addresses. Pointers in C. Memory is just a sequence of byte-sized storage devices.

p q r int (*funcptr)(); SUB2() {... SUB3() {... } /* End SUB3 */ SUB1() {... c1: SUB3();... c3 c1 c2: SUB3();... } /* End SUB2 */ ...

Advanced C Programming

MICRO-SPECIALIZATION IN MULTIDIMENSIONAL CONNECTED-COMPONENT LABELING CHRISTOPHER JAMES LAROSE

Run-time Environments - 3

Hacking in C. Pointers. Radboud University, Nijmegen, The Netherlands. Spring 2019

Impact of Dependency Graph in Software Testing

Chapter 2 (Dynamic variable (i.e. pointer), Static variable)

C Programming Language: C ADTs, 2d Dynamic Allocation. Math 230 Assembly Language Programming (Computer Organization) Thursday Jan 31, 2008

Aryan College. Fundamental of C Programming. Unit I: Q1. What will be the value of the following expression? (2017) A + 9

Analysis of Object-oriented Programming Languages

IMPACT OF DEPENDENCY GRAPH IN SOFTWARE TESTING

Lecture Notes on Liveness Analysis

Pointers II. Class 31

Writing an ANSI C Program Getting Ready to Program A First Program Variables, Expressions, and Assignments Initialization The Use of #define and

Chapter 5. Names, Bindings, and Scopes

C Review. MaxMSP Developers Workshop Summer 2009 CNMAT

CS Programming In C

C Programming Review CSC 4320/6320

Run-Time Environments/Garbage Collection

Advanced Compiler Construction

Towards Locating a Functional Concern Based on a Program Slicing Technique

Programming Logic and Design Sixth Edition

Transcription:

Program Slicing in the Presence of Pointers James R. Lyle David Binkley jimmy@sst.ncsl.nist.gov binkley@sst.ncsl.nist.gov U.S. Depar tment of Commerce Technology Administration National Institute of Standards and Technology Abstract--This paper reports on computing program slices for languages with pointers. We use a variation on symbolic execution to assign addresses to pointer variables and to build lists of possible addresses contained by each pointer at each statement. The number of lists is kept small by using a concept similar to control flow based basic blocks called basic pointer blocks, acon- tiguous set of statements headed by an assignment to a pointer variable. Index Terms--Program slicing, data flow analysis, debugging, pointers, software tools, software engineering. I. INTRODUCTION Program slicing is a family of program decomposition techniques based on extracting statements relevant to a computation in a program. Program slicing as originally defined produces a smaller program that reproduces a subset of the original program s behavior [18]. This is advantageous since, by excluding irrelevant statements, the slice can collect an algorithm that may be scattered throughout a program. It should be easier for a programmer interested in a subset of the program s behavior to understand the corresponding slice than to deal with the entire program. The utility and power of program slicing comes from the potential automation of tedious and error prone tasks. Program slicing has applications to program debugging [19, 20, 15, 21, 18], program testing, program integration [8], parallel program execution [18], software metrics [18, 16], reverse engineering [2], and software maintenance [6]. Several variations on this theme have been developed, including program dicing, dynamic slicing [1, 13] and decomposition slicing. The most important goals of research in program slicing are the following: tocompute slices faster, tofind algorithms that produce smaller slices, todevelop new applications of program slicing, and, tocompute slices for a wider collection of constructs. There have been many papers on applications of program slicing, Also at Loyola College in Maryland and improvements to algorithm speed and precision [5, 9], but few papers have addressed difficult language constructs such as pointers [10, 12, 3, 14]. Program slicing cannot become a useful tool until slicing algorithms can deal with real world program features such as pointers. This paper presents an algorithm for computing program slices for programming languages that use pointers. An early version of this pointer tracking algorithm has been implemented as part of unravel, a program slicing tool developed at NIST. The goal of our work is to produce a slicing algorithm that is useful on real code (ANSI C). A. Definitions Definition: A slicing criterion for a program slice is a tuple < i, V >where i is a statement in the program and V is a subset of the program variables. A slice of a program is computed with respect to v, atstatement i. Definition: A program slice of program P for slicing criterion, <i,v>, is an executable program obtained by deleting zero or more statements from P such that the values of the variables in V are the same when execution reaches statement i of both P and the slice of P [18]. Definition: Refs(n) is the set of variables referenced (the variable s value is used) at statement n. Definition: Defs(n) is the set of variables defined (the variable is assigned a value) at statement n. Definition: Succ(n) is the set of statements that could be executed immediately after statement n. Definition: Irefs(n) is the set of pointer variables dereferenced that have values used at statement n. The set members are represented as ordered pairs of the form (v,k), where v is a pointer variable and k is the level of dereferencing (i.e. v has k * operators applied). Definition: Idefs(n) is the set of pointer variables dereferenced that have values assigned at statement n. The set members are represented as ordered pairs of the form (v,k), where v is a pointer variable and k is the level ofdereferencing (i.e. v has k * operators applied). B. Background Computing static slices from programs that contain pointers raises several issues not found in slicing other programs. First,

1typedef struct List_struct List; 2struct List_struct { 3 int value; 4 List *next; 5}; 6 7main() 8{ 9 List *at,*head =NULL,*node; 10 int n; 11 12 n =0; 13 while (n < 10){ 14 node =(List *) malloc (sizeof(list)); 15 node -> value = n++; 16 node -> next = head; 17 head =node; 18 } 19 at =head; 20 at -> value = 47; 21 at =at->next; 22 at -> value = 8; 23 printf ("%d\n",at -> value); 24 } Figure 1. what is the meaning of slicing on a field of a dynamic structure? Does the criterion refer to a specific memory location or to a class of locations? Second, does the slice need to include all statements required to make the slice executable? Can conditions be inserted in a slice to make the slice executable? We present two examples to illustrate these issues. It is not possible to keep track of exactly where a pointer in alinked list points. However, since the statements involved in the creation and manipulation of a linked list could operate on any of the nodes in the linked list, keeping detailed information about which list node each node points to may not enable a slicing algorithm to compute a smaller slice. We recognize two types of addresses, addresses of static objects and addresses of dynamic objects, each address type requires a different approach. The essential difference is that addresses of dynamic objects cannot easily be tracked precisely and an indirect assignment through a dynamic address must be treated like anassignment to an array element. A slicing criterion using a dynamic object can be ambiguous. For example, consider the slicing criterion, <23,at->value> on the program in Figure 1. The slice should consist of statements: {12, 13, 14, 16, 17, 18, 19, 20, 21, 22}. However, if the assignment to at->value is treated as a scalar then lines 20 and 15 are not included in the slice. If the slicing criterion means slice on the value member that at currently points to, then lines 20 and 15 should not be included in the slice. However, if the slicing criterion means slice on any value member that at might point to, then lines 20 and 15 should be in the slice. Consider a slice on s at line 20 in Figure 2. The slice should consist of at least the following set of statements: {1, 2, 6, 9, 14, 15, 17, 18, 21}. This slice has the problem that by leaving 1main() 2{ 3 int a,b,s,u,x; 4 int *w,*y,**z; 5 6 s=1; 7 a=2; 8 b =3; 9 scanf ("%d %d",&x,&u); 10 11 if (x) y = &a; 12 else y=&b; 13 14 if (u) z = &y; 15 else z=&w; 16 17 w =&s; 18 **z =4; 19 20 printf ("a %d b%ds%d\n",a,b,s); 21 } Figure 2. lines 11 and 12 out of the slice, the slice is not executable since *z might be undefined and cause a run-time error when attempting to assign to * * z. The problem is with the statement on line 18. The variable z could contain either the address of y or the address of w. If z contains the address of w then executing line 18 sets the value of s since w contains the address of s. However, if z contains the address of y then executing line 18 has no effect on the value of s. Lines 11 and 12 are not relevant to the computation of s; anything could be executed so long as y is given the address of a variable not used in the slice. Since only w contains the address of s, line 18 is only in the slice when the condition z &w is true. A program slicer producing executable slices has two possible solutions: (1) Add statements to the slice that are irrelevant to the slicing criterion but are required to keep the slice executable. (2) Add conditions to guard against executing statements that would cause a run-time error when the slice is executed. C. Algorithm Overview Our approach for ANSI C exploits how pointers are used in real world programs. There are several common patterns of usage that can be exploited to produce practical pointer slicing algorithms. A pointer variable used to address dynamicly created instances of a structure usually does not point to other structure types. Statements that change the pointer state by allocating dynamic storage, taking an address or assigning one pointer variable to another are sparsely distributed throughout a program. Pointer variables usually point to a small set of possible addresses. Final - 2- September 16, 1993

The algorithm requires a pre-pass over the program to compute lists of possible address values for each pointer at each statement in the program. The slicing algorithm must be modified to not only keep track of data but also addresses. Our algorithm is similar to one developed by [17]. These modifications can be made to either a control flow based slicing algorithm [18] or a PDG [5] based slicing algorithm. II. CHANGES TO THE SLICING ALGORITHM We present our changes in the context of a recursive formulation of program slicing for a language subset of ANSI C without if statements or loop statements. The formulation can be easily extended for compound control statements by associating a list of statements, requires(n), required to be included in the slice containing statement n. The requires set is a generalization of control dependence [5]. This recursive formulation is convenient for discussing program slicing but, it is not a practical implementation algorithm. Let v be some program variable name, n be some statement. Let P i (n, v) beafunction that returns the set of addresses that *...*v (where there are i * s)could point to at statement n. P 1 is understood if no subscript is written. Section III presents how to keep track of addresses that might be used by pointer variables so that P can be computed. Note that P 0 (n, v) = {v } and for i >1, P i (n, v) = {x x P 1 (n, y) y P i 1 (n, v)}. Recursive Slicing Algorithm: S <m succ(n),v> = {n} S <n,v> {n} S <n,x> x refs(n) if v defs(n) & refs(n) = if v / defs(n) ifv defs(n) & refs(n) Definition: The active set at statement n, active(n), is the union of all variables x found in slicing criterion on statement n induced by the recursion. The active set at statement n is the set of all variables that could influence any statement in the slice executed after statement n. Any variable not in the active set at statement n can be safely changed without affecting the computation of the slice. Introduction of pointers changes the treatment of assignments and references to variables. A reference to *B is a reference to B and a possible reference to any object that B might point to. We have two cases, how pointers change treatment of ref sets and how pointers change treatment of idef sets. A. Indirect references Suppose we have a statement such as: n: A = *B + C; Consider the slicing criterion: S < m, v >where m succ(n) and v active(m). If A / active(m) this statement does not belong in the slice and can be passed by. If A active(m) & A defs(n) the slice is the statement, n, unioned with the following: S <m=succ(n),x> ={n} S <n,v> v refs(n) S <n,v> v active(m) &v / defs(n) S <n,v> v P(n, B) The last member of the above union generalizes to k levels of indirection by: S <n,v> v P i (n, b) i,1 i k (b, k) irefs(n) B. Indirect assignments Suppose we have a statement such as: n: *B = A; Consider the slicing criterion: S < m, v >where m succ(n) and v active(m). If active(m) P(n, B) statement n should be included in the slice, unioned with the following: S <m=succ(n),x active(m)> ={n} S <n,v> v refs(n) S <n,v> v P(n, B) S <n,b> If there are at least two members of P(n,B), say, y / Xand z X, the slice S <n,p(n,b)>,captures the case where B actually points to a variable (y in this example) not in X and z should remain in the active set since statement n does not change the value of z. III. CAPTURING THE POINTER STATE Extending program slicing to programs using pointers requires that it be possible to determine where pointer variables might be pointing at each program statement. Addresses are introduced into the pointer state from two sources, applying the address of operator (&) to a declared variable and the address of storage allocated from the heap returned by malloc class functions. The information on addresses is collected by identifying statements that introduce addresses into a program and the addresses are then propagated through the program to obtain the pointer state for each statement. This is somewhat like symbolic execution [11, 4] in that possible values of variables (pointer variables only) are determined by static program analysis rather than by actual program execution. It is useful to observe that embedded within the program flow graph is a subgraph that represents generation and manipulation of addresses. Every program statement does not have to beanalyzed since only the statements corresponding to nodes of this subgraph need to be analyzed to determine the pointer state. This subgraph is smaller, usually much smaller, than the original program. Definition: The Pointer State Subgraph (PSS) is obtained for a control flow graph by deleting nodes that correspond to statements that do not assign a pointer value and by deleting any branch node and corresponding join node if all nodes from the branch node to the corresponding join node have been deleted. First, we consider the pointer state for addresses of declared variables, then addresses allocated from the heap. A. Addresses of Declared Variables Given that n is a statement, v is a variable, and Q(n, v) = {all currently known variables that v might point to after statement n executes}. The function Q is used to accumulate information about the pointer state. Eventually Q becomes P, the pointer state function used by the slicing algorithm. Q i is defined analogously to P i. Final - 3- September 16, 1993

The algorithm for capturing the pointer state is the following: 1. Construct the PSS 2. Scan the PSS for address creation 3. Propagate created addresses 4. Repeat step 3 until no more changes occur The pointer state of each node is propagated from that node to all successor nodes by taking the union of the pointer state for all variables not defined at the node with any changes required by the propagation rule for the kind of statement corresponding to the node of the PSS. The following table gives the basic address propagation rules. B. Addresses From The Heap Consider line 14 of Figure 1: 14 node =(List *) malloc (sizeof(list)); The variable node is receiving the value of a different address of storage on the heap each pass through the loop at lines 13--18. It is not possible to determine by static analysis how many times storage is dynamically allocated from the heap or to track assignments to individual locations. This implies that an approximation that combines together several dynamically allocated storage locations should be introduced. For example, the heap could be treated as a single array with assignments to individual members treated as both an assignment and a reference or tracked with methods such as [7]. However, treating the heap as a single array is too conservative; bypartitioning the heap into several arrays we can do much better. The heap should be partitioned by object type so that all objects of the same type are treated as if they have been collected into a single array. Then the entire array of objects of a single type from the heap can be treated as a single object of a given type with a change to a dynamic object treated as an update to an array. It is not easy to identify the type of object that a chunk of dynamically allocated storage is being used to contain since routines such as malloc only returns an address to a block of storage of the requested size. This address may be directly assigned to a pointer of the matching type or may be assigned by a cast operation to a pointer of another type or even to an integer variable. Such pointer laundering, casting a pointer to another pointer type or even non-pointer type so that the true type of the pointer is Statement Propagation Rule A=&x Q(n,A) = {x} A=B Q(n,A) = Q(n,B) A=*B Q(n,A) = Q(n, X) X Q(n, B) A =**B Q(n,A) = Q(n, X) X Q(n, Y ) Y Q(n, B) A=*...*B Q(n,A) = Q k (n, B) where k is indirection level *A = &x Q(n,Y) = Q(n,Y) {x}, Y Q(n, A) **A = &x Q(n,Z) = Q(n,Y) {x}, Z Q(n, Y ) Y Q(n, A) *...*A = &x Q(n,Z) = Q k (n, A) {x}, where k is indirection level obscured, requires that all assignments of values that are used as pointers must be tracked to ensure that the pointer state is correctly captured. To capture the pointer state, introduce a pseudo-variable address for each address allocation point in a program. This is usually every call to a malloc class function, however, it might be the case that a program has its own memory allocation function to allocate storage for program objects. The memory allocation function calls malloc or directly obtains heap storage from the run time environment. For such a program, each memory allocation function invocation should have a pseudo-variable introduced. IV. SUMMARY The contributions of this paper are the changes required to incorporate pointers in program slicing algorithms, and the address propagation rules for the pointer state subgraph. The pointer state subgraph provides a significantly smaller graph for analysis to capture the pointer state than the entire program flow graph or a PDG. This algorithm gives efficient treatment of pointers for slicing programming languages such as ANSI C. References [1] H. Agrawal and J. R. Horgan, Dynamic Program Slicing, Proceedings of the ACM SIGPLAN 90 Conference on Programming Language Design and Implementation, pp. 246-256 (June 20-22, 1990). [2] J. Beck and D. Eichmann, Program and Interface Slicing for Reverse Engineering, Proceedings Working Conference on Reverse Engineering, Baltimore, Maryland (May 21-23, 1993). [3] D. R. Chase, M. Wegman, and F. K.Zadeck, Analysis of Pointers and Structures, Proceedings of the ACM SIG- PLAN 90 Conference on Programming Language Design and Implementation, pp. 296-310 (June 20-22, 1990). [4] L. A. Clarke, A System to Generate Test Data and Symbolically Execute Programs, IEEE Transactions on Software Engineering SE-2 p. 215 222 (Sept. 1977). [5] J. Ferrante, K. Ottenstein, and J. Warren, The Program Dependence Graph and its Use in Optimization, ACM TOPLAS 9(3) pp. 319-349 (July 1987). [6] K. B. Gallagher and J. R. Lyle, Using Program Slicing in Software Maintenance, IEEE Transactions on Software Engineering 17(8) pp. 751-761 (August 1991). [7] R. Hamlet, B. Gifford, and B. Nikolik, Exploring Dataflow Testing of Arrays, Proceedings 15th International Conference on Software Engineering, Baltimore, Maryland (May 21-23, 1993). [8] S. Horwitz, J. Prins, and T. Reps, Integrating Noninterfering Versions of Programs, ACM TOPLAS 11(3) pp. 345-387 (July 1989). [9] S. Horwitz, T. Reps, and D. Binkley, Interprocedural Slicing Using Dependence Graphs, ACM TOPLAS 12(1) pp. 26-60 (January 1990). Final - 4- September 16, 1993

[10] S. Horwitz, P. Pfeiffer, and T. Reps, Dependence analysis for pointer variables, Proceedings of the ACM SIG- PLAN 89 Conference on Programming Language Design and Implementation, (Portland, OR, June 21-23, 1989), ACM SIGPLAN Notices 24(7) pp. 28-40 (July 1989). [11] W. E. Howden, Symbolic Testing and the DISSECT Symbolic Evaluation System, IEEE Transactions on Software Engineering SE-3 pp. 266-278 (1977). [12] J. Jiang, X. Zhou, and D. Robson, Program Slicing For C -- The Problems In Implementation, Proceedings Conference on Software Maintenance 1991, Sorrento, Italy (October 15-17, 1991). [13] B. Korel and J. Laski, Dynamic Program Slicing, Information Processing Letters 29(3) pp. 155-163 (October 1988). [14] W. Landi and B. Ryder, Safe Approximate Algorithm for Interprocedural Pointer Aliasing, Proceedings of the ACM SIGPLAN 92 Conference on Programming Language Design and Implementation, pp. 235-248 (June 1992). [15] J. R. Lyle and M. Weiser, Automatic Program Bug Location by Program Slicing, Proceedings of Second International Conference on Computers and Applications, Peking, China (June 1987). [16] L. Ott and J. Thuss, Slice Based Metrics for Estimating Cohesion, Proceedings First International Software Metrics Symposium, Baltimore, Maryland (May 21-23, 1993). [17] William E. Weihl, Interprocedural Data Flow Analysis in the Presence of Pointers, Procedure Variables and Label Variables, Conference Record of the Seventh Annual ACM Symposium on Principles of Programming Languages, pp. 83-94 (January 1980). [18] M. Weiser, Program Slicing, IEEE Transactions on Software Engineering, SE-10(4) pp. 352-357 (July 1984). [19] M. Weiser, Programmers Use Slices When Debugging, CACM 25(7) pp. 446-452 (July 1982). [20] M. Weiser and J. R. Lyle, Experiments on Slicing-Based Debugging Aids, pp. 187-197 in Empirical Studies of Programmers, ed. E. Soloway and S. Iyengar,Ablex Publishing Corporation, Norwood, New Jersey (1986). [21] M. Weiser, Program Slicing, Proceedings of the Fifth International Conference on Software Engineering, pp. 439-449 (March 1981). Final - 5- September 16, 1993