Compiler-Assisted Software Verification Using Plug-Ins

Sean Callanan, Radu Grosu, Xiaowan Huang, Scott A. Smolka, and Erez Zadok
Stony Brook University



We present Protagoras, a new plug-in architecture for the GNU compiler collection that allows one to modify GCC's internal representation of the program under compilation. We illustrate the utility of Protagoras by presenting plug-ins for both compile-time and runtime software verification and monitoring. In the compile-time case, we have developed plug-ins that interpret the GIMPLE intermediate representation to verify properties statically. In the runtime case, we have developed plug-ins for GCC to perform memory leak detection, array bounds checking, and reference-count access monitoring.

1  Introduction

In this paper we discuss compiler-assisted instrumentation, a form of instrumentation in which the compiler is enlisted to insert patch code. Patch code is inserted at specific locations in an existing program for diagnostic or repair purposes without altering its source code. The instrumentation is performed as the compiler transforms the source code into object code. The compiler is highly suitable for use in instrumentation for two reasons. First, it is aware of the source-level structure of the program being instrumented, and can therefore be both flexible and precise in selecting points at which patch code should be inserted. Second, it is aware of the assignment of variables to registers, making it easy for patch code to extract information about its environment. This makes it possible to instrument and debug code with full optimization, for instance.
We also present a technique called compiler-assisted verification, in which compile-time information is used to verify the correctness of a program while its intermediate representation is transformed. The compiler is a particularly suitable environment for verification because the internal structure of the program in the compiler is in an easy-to-parse format that retains much of the program's high-level semantics while nevertheless being reducible to the actual generated code accurately.
Compiler-assisted instrumentation and verification have the following three advantages:
Versatility: Access to the full parse tree and control flow graph of a program allows instrumentation of a wide variety of code patterns with full type information; furthermore, it allows verification of any program in any language supported by the compiler.
Accuracy: Instrumentation can be used in combination with full compiler optimization, making results as close as possible to the uninstrumented program. Verification is performed on the same intermediate representation that is compiled to make the executable.
Speed: Compiler-assisted instrumentation makes monitoring functionality part of the program itself.
We believe that the potential exists for many compiler-assisted instrumentation tools with applications in a wide range of areas. We have developed a plug-in architecture called Protagoras, which permits insertion of new code into GCC, the GNU compiler collection [5]. (Protagoras is named after the first Greek sophist, who was interested in using criticism and modification to extract hidden truths from arguments.) GCC is the compiler of choice for most open-source software, and is also used for many commercial programs, including the MacOS-X operating system. We are developing program understanding, verification, and debugging tools using Protagoras. However, GCC's internals were not designed with this kind of tool in mind, so the learning curve for writing such a tool can be quite steep. In this paper, we present relevant aspects of GCC's internals, as well as several examples of instrumentation tools we have developed.
The rest of this paper is structured as follows. In Section 2 we present related techniques and compare them to compiler-based instrumentation and verification. In Section 3 we outline the facilities that GCC provides for analysis of the GIMPLE intermediate representation. Instrumentation tools use these to construct code, and verification tools use them to check code. In Section 4 we describe Protagoras's role in compilation, and we enumerate several plug-ins we have created using it. Finally, in Section 5, we discuss future tools we would like to implement, as well as extensions to GCC which would further facilitate compiler-assisted instrumentation and verification.

2  Related Work

Instrumentation is not limited to the compiler. After the binary has already been generated, tools like the ATOM library are capable of instrumenting arbitrary binaries [11]. Such tools can be used with programs whose source is not available. However, to find locations to instrument, one must know the specifics of how the binary was generated by the compiler. Additionally, it is difficult (and, in the case of optimized code, virtually impossible) to extract higher-level information from assembly code. Instrumentation can also be performed by modifying a program's source code directly. For example, Lockmeter wraps the spinlock-access macros in the Linux kernel with accounting code [2]. The wrappers record how much time was spent waiting for the lock, how long the lock was held, and where in the code it was taken. Finally, instrumentation can be added at runtime, such as with DTrace [3]. However, this requires that instrumentation hooks have been inserted into the program.
Debuggers are a competing technology for instrumentation. Compilers already provide debugging information, which contains information about a program's stack structure and original source code, and can be used by debuggers to investigate the state of a paused or crashed program. However, a debugger is an inefficient mechanism for monitoring and verifying properties at many points in a program. There are three reasons for this. First, because the debugger and the program run in separate memory spaces, there are two context switches at each instrumentation point. Second, the debugger must traverse the debugging data structures, which are frequently complex. Third, in some common debugging formats like STABS, debugging information is spread throughout the binary [10].
Compiler-assisted instrumentation itself is not a new idea: the gprof utility also uses the compiler to instrument a program [6]. gprof relies on the GNU C compiler to add patch code to each location where a function returns to its caller. This patch code increments a counter corresponding to the current caller, callee pair. These counters are stored in a file which can then be processed using gprof to provide a weighted call graph for the program. In a similar fashion, the gcov tool provides coverage information for all code lines, as well as information about how often conditionals were satisfied [4].
A great deal of work has been done on model checking; however, most model checkers verify properties in programs written in specialized modeling languages. The SLAM toolkit perform model-checking on C [1]; however, since it is not integrated into a compiler, the variant of C that it supports does not track the style of C that is used in many programs, particularly open-source software. A model checker that resides in the middle-end of a production compiler is able to check all programs supported by the compiler, which, in GCC, means programs in C++, Java, and Fortran, in addition to C.

3  Code Analysis with GCC

Figure 1 shows the phases of compilation in GCC version 4. We will briefly describe what is done at each level, and describe the information that is available to compiler-aided instrumentation and verification code at that level.
Figure 1: GCC Architecture
Source code is accepted from the preprocessor, and passed to a language-specific parser. The parser transforms the code into a language-dependent tree representation, which is either the GENERIC intermediate representation or is converted into GENERIC at the end of parsing [9]. GENERIC is a language of complex statements, where expressions can be nested several statements deep. To simplify processing, GENERIC is lowered, or decomposed, into a three-address code called GIMPLE. GIMPLE is a subset of GENERIC with temporary variables created to hold intermediate values of computations. This process is known as gimplification. GCC then builds a control-flow graph from the GIMPLE code and passes the resulting structure to the backend for conversion into RTL, an assembly-like notation which is well-suited for subsequent conversion to native code. Code can be modified and optimized at each of these three layers:
In our work thus far, we have used the middle-end, and specifically the GIMPLE/CFG level. This was done for three reasons. First, the middle-end has access to high-level semantic information such as symbol names and types, making it easier to find specific locations in the source code without needing to know details of how they are transformed into assembly. These details differ between compilers and even compiler versions. Second, the middle-end is both language and platform independent, making code written there portable to all languages and platforms which GCC supports. Third, APIs in the middle-end, in particular the GIMPLE intermediate representation, are deliberately preserved between GCC versions. This is because they are used by GCC's tree optimizations, such as loop unrolling.
Figure 2: The GIMPLE CFG and tree traversal API
The GIMPLE intermediate representation of a function has the structure of a control flow graph, as seen in Figure 2. The basic_block structure contains connections to other basic blocks, as well as forward and backward pointers representing the order in which basic blocks appear in the code. These can be traversed using the FOR_EACH_BB macro. Each basic block consists of statements, which may be traversed using a statement iterator. The following example code traverses an entire function:
basic_block bb;
block_stmt_iterator iter;

  for(iter = bsi_start(bb);
      bsi_next(&iter)) {
    tree stmt = bsi_stmt(iter);

The tree structure is the central data structure in GIMPLE, representing a node in the abstract syntax tree. Trees can represent expressions, types, and declarations, among other syntactic structures. The full set of tree types is specified in the file tree.def, which contains lines of the form:
DEFTREECODE(id, name, flags, parameters);

These can be used to handle all possible GIMPLE tree types by defining DEFTREECODE as a macro as in the following example:
#define DEFTREECODE(i, n, f, p) \
  case i: walk(subtree, n);     \

void walk_expr(tree curr, int numargs)
  in i;
  for(i = 0; i < numargs; i++) {
    tree subtree = 
      TREE_OPERAND(curr, i);
      switch(TREE_CODE(subtree)) {
        #include <tree.def>

This function uses the entries in tree.def as the cases for a large switch statement which is applied to all subtrees of the current tree, passing the number of subtrees of the subtree to itself in a recursive manner.
In the rest of this section, we describe the technical aspects of creating trees. Although these are presented as ways of creating GIMPLE trees, the techniques also apply equally to the GENERIC abstract syntax tree, because GIMPLE is a subset of GENERIC. We first discuss types, which must be specified for all expressions, in Section 3.1. We discuss the basic API for creating an expression node, the buildn macros, in Section 3.2. Finally, we discuss function calls in Section 3.3.

3.1  Types

Every GIMPLE expression has a type. These types can be specific to a particular language, but there are also common types. The TREE_TYPE macro returns the type of an existing expression, suitable for reuse. Some standard C integer types, like unsigned_char_type_node, are defined in tree.h. Pointer types can be derived by applying build_pointer_type to an existing type. Compound types can be constructed via macros; for example, the following example code creates a type node for an array of three characters:
tree array3 =

When calling a function that has not yet been seen (such as when inserting patch code that will be linked in), one must construct a function declaration for it. This involves both creating a symbol name for it and declaring its type. The build_function_type function takes two parameters: the return type and the parameter types. In practice, however, the return type is the only important type: the parameter types can be omitted. The following code creates a declaration for a function named __logger, which returns void.
tree logger_name =

tree logger_type =
    NULL_TREE /* omitted */

tree logger_decl =

3.2  Expressions

Once a type has been derived from an existing expression or created anew, its corresponding expression can be constructed. The simplest expression types are constants, for which special helper functions usually exist. For example, the build_int_cst function takes a type and an integer value, and returns a tree corresponding to that integer constant. A string constant is constructed using the build_string function, but its type must be set manually using the TREE_TYPE macro as follows:
TREE_TYPE(mystr) = array3;

Another basic expression type is a variable access. In many cases, variables are reused by extracting them from an existing function. However, if a new variable is to be constructed, this can be done by constructing a declaration in a similar way to the way a function is declared, except that the type should not be a function type and VAR_DECL should be passed to build_decl instead of FUNCTION_DECL.
There is a separate tree type for each possible operator. For example, PLUS_EXPR represents binary addition, and ADDR_EXPR takes the address of its operand. It is important to bear in mind that at this point in the compilation process, GCC cannot infer the type of such an expression, and the type must be explicitly specified. The buildn family of macros constructs n-ary tree nodes; for example, the following code creates a tree that represents the sum of two integers:
tree sum_tree =

For some purposes, such as when specifying the parameters of a function, a variable-length list of elements is required. In this case, a container node is required: specifically, a TREE_LIST. Lists are created by using the tree_cons function. We show how to construct a list in Section 3.3.

3.3  Function Calls

A function call is one of the more complex constructs in GIMPLE. The function declaration must first be constructed, as seen in Section 3.1. Next, a function pointer must be constructed using special type qualifiers as follows:
tree logger_pointer =

The parameters are then composed into a list using the tree_cons function. This function takes three parameters. The first is a key called a purpose, which is unused in function calls. The second parameter is the value of the list entry. The third parameter is the list to prepend it to (or NULL if a new list is to be created). Finally, the function call is constructed using the build_function_call function:
tree arguments =
  tree_cons(NULL, mystr, NULL);

new_call =

4  Applications

Having discussed the fundamental techniques used to implement compiler-assisted instrumentation and verification, we now turn to some applications we have developed using this technique. For these applications, we have developed a plug-in architecture for GCC called Protagoras. Protagoras is a set of modifications to the compiler, which allow it to load plug-ins that analyze or modify the GIMPLE representation of each function after the control-flow graph has been generated. We also modified the build process of GCC to make the compiler export symbols to plug-ins. We instrument code by adding calls to separately compiled patch functions which are linked in after compilation. The architecture of this system is shown in Figure 3.
Figure 3: Our instrumentation system. The components of Protagoras are highlighted.
This system has two advantages, both directly related to speed of development. First, the development of plug-ins separately from the GCC code-base eliminates time-consuming link phases. Second, the use of separately compiled and linked patch code obviates the need to construct large amounts of C code in raw GIMPLE, which is a time-consuming and error-prone process as seen in Section 3.
We will discuss three applications of compiler-assisted instrumentation: a malloc() debugger in Section 4.1, a bounds checker in Section 4.2, and a reference-count checker for the Linux kernel in Section 4.3. Additionally, we will discuss an example of compiler-assisted verification, a model-checking plug-in that interprets GIMPLE code, in Section 4.4.

4.1  malloc() Debugging

We have implemented a malloc debugger for GCC that locates all uses of the malloc and free functions in a program's source code and appends a call to an appropriate logging function which is defined in a separate static library as shown in Figure 3. While the program runs, the logging functions maintain a list of existing allocations; it reports any invocations of free on unallocated areas, and any malloced areas that have not been freed by the end of the program's execution.
The way we perform the instrumentation of a malloc invocation in GCC is by deconstructing its parent assignment expression. Even if the result of the malloc is directly passed to a function, say f, in the C source code, in the GIMPLE representation it is put into a temporary variable, which in turn is passed to f. The tree representing the assignment of the result of malloc to a variable is a MODIFY_EXPR, with the variable as its first parameter and a CALL_EXPR to malloc as the second. We construct a call to the logger, passing the result variable and the parameter of the malloc call-which specifies the size of the area-as well as the location of the call in the C source code.
The location of a particular GIMPLE expression in the original C source code can be extracted using the EXPR_LINENO and EXPR_FILENAME macros. Additionally, we can check for coding anomalies like passing a literal to free by checking what kind of a tree the parameter to free is. We dispatch compiler warnings using the warning function.

4.2  Bounds Checking

We have also implemented a bounds checker for GCC that identifies all valid memory areas in the text segment of a binary, all stack areas, and all heap allocations used by a function; it then inserts patch code to verify that every memory access of that function lies within those bounds. These areas are not limited to arrays; we also allow dereferencing of char pointers that point to portions of a 32-bit int variable, for instance. As in Section 4.1, this is achieved using logging functions: one to register an area, one to deregister an area, and one to check an access for validity, while the program is executed.
In the case of heap areas, accounting is simple, since heap areas are made valid explicitly through the malloc function and invalidated using the free function. Text areas are registered at the start of the first function that uses them; stack areas are registered at the start of their corresponding functions, and deregistered when the functions exit. We invoke a compiler pass to collect all variables referenced in a function as follows:

for(i = 0;
    i++) {
  tree variable = 
    VARRAY_TREE(referenced_vars, i);
    /* temporary variable */
  else if(TREE_STATIC(variable))
    /* text variable */
    /* stack variable */

Now that accounting for stack, heap, and text areas has been inserted, what remains is to instrument pointer dereferences. This is done by finding tree entities with type INDIRECT_REF. The argument of the dereference operator is passed to the validator function, which reports an access that is not inside a valid area.

4.3  Reference Count Verification

We implemented a tool that locates modifications of reference counters in the Linux kernel and verifies the correctness of these operations, as well as checking for leaks, while the system runs. A reference count leak causes not only resource leakage but also faulty system operation as synchronization based on reference counts malfunctions [12]. This tool identifies all locations where variables of type atomic_t (the type used in the Linux kernel for reference counters) are modified. Linux includes a set of functions that modify these variables correctly, but atomic variables are modified directly without using these functions at several locations in the kernel code.
The type name for a complex type can be extracted using the TYPE_NAME macro, but it is represented as an identifier node. As a result, the IDENTIFIER_POINTER macro must be applied to the identifier to return a standard C string. Since Linux kernel atomic types are not standard C types, we must compare this name with the string atomic_t to locate atomic variables.
Our reference-count monitor computes error rates and disables instrumentation once a high enough confidence has been achieved that the error rate is very close to zero. Additionally, we found it desirable to maintain per-category confidence levels. Different kinds of objects are handled by different parts of the kernel, and these parts may be different both in the frequency with which they access reference counters and in their correctness. To do this, we needed a way to determine the container object type of a reference count. If the reference counter is being directly accessed inside a structure, then we can simply look at its parent structure's type as follows:
tree container_type = 
    TREE_OPERAND(object, 0)

However, if the address of the reference counter has been placed in a temporary variable, then we must keep track of the type of the container of the object whose address is in the temporary. To do this, we maintained a hash of tree nodes to container types, which is updated each time the address of an atomic_t is placed into another variable.

4.4  Model Checking

The GCC middle-end is useful for more than just instrumentation tools and optimizers that change the runtime characteristics of an application. We have also developed a Protagoras plug-in application that performs compiler-assisted verification: a model checker named GMC2. It operates on the gimplified source code of a concurrent program and performs multiple randomized executions on a simulated machine supporting channel-based IPC [7].
The GMC2 model checker begins interpreting the GIMPLE source code of a program at a fixed initial state. Whenever the active thread in the interpreted code invokes a concurrency primitive, such as thread creation or inter-process communication, a context switch occurs, allowing a different, randomly-selected thread to run. The state of the system at this point is stored. If a user-specified property is violated while the program is being interpreted, GMC2 records a failure and resets the program's state. It records a success when a previously observed state is seen again-that is, a lasso occurs-with no failure having been observed in the previous execution. Confidence in the overall success rate increases as the number of recorded lassos increases. In contrast to other model checkers, GMC2 need only remember those states observed in the current lasso, taking advantage of the fact that the randomized executions are independent of each other and error rates are therefore meaningful.
GMC2 executes GIMPLE source code, so additional intermediate data is required besides the compile-time information provided by the GIMPLE API. We have added a hash table which assigns values to all variables currently in scope. This hash table is used to interpret all GIMPLE statements. Using the GIMPLE API instead of the GENERIC API sharply reduces the number of different expression types we must handle, and also allows us to interpret any language that GCC supports without adding language-specific interpretation code.
Another challenge we faced while developing this system was that GCC transforms code one function at a time. Normally, the resources used to hold the GIMPLE representation of one function are reused when the next function is parsed. This is a problem for interpreters, which require the entire program to be available. For this reason, we interrupt execution at two points: when each function has been gimplified and is about to be optimized, and when all functions have been processed. At the first point, a function's GIMPLE representation is stored in a separate data structure which is preserved throughout the compilation. At the second, the code in the resulting data structure is interpreted.

5  Future Work

Section 4 merely documents a preliminary exploration of the full space of potential applications for compiler-assisted instrumentation. However, it is sufficient to demonstrate the viability of compiler-assisted instrumentation in general, and the use of our plug-in architecture, Protagoras, to insert code into the GCC middle-end in particular. It also identified several areas in which the existing infrastructure is lacking and could be extended, to exploit the strengths of compiler-assisted instrumentation while mitigating its weaknesses.
In Section 5.1 we describe two proposed applications of this technique, including selected choices and challenges presented by their implementation. In Section 5.2 we describe two techniques we plan to investigate for improving GCC's support for compiler-assisted instrumentation and verification tools.

5.1  New Applications

Data structure access logger   We will modify every variable modification, and, as the instrumented program runs, it will generate a log file which provides a detailed record of when each variable was modified, where in the code it was modified, and what it was set to.
We will design a tool to parse these files. It will have the interface of a standard debugger, with two major differences. First, as the run of the application will have been logged, it will be completely reproducible. This will make it feasible, for instance, to transmit a detailed log of an application's entire run from a test engineer to a developer, simplifying the task of finding a bug. Second, the run will be traversable both forward and backward in time, allowing, for instance, reverse watchpoints, allowing a programmer to trace the provenance of an anomalous value of a variable.
The implementation of this tool could be completely at the GIMPLE level; however, the tool would need to handle library functions like memcpy separately. Additionally, because the amount of data generated by this tool could potentially be quite large, this tool will use bandwidth-reduction techniques like compression, snapshotting, and pattern-based encoding (such as recording the first and last values of a loop counter instead of each value). Finally, if only specific variables or variables of a particular type are of interest, the tool could be instructed to filter on that basis.
Thread hang detector   We will instrument every loop, and determine the conditions for leaving each loop. Additionally, the instrumentation tool will enumerate the variables that would be modified if the loop were left. Based on that information, a multi-threaded application will be executed in parallel with a monitor, which will gather information about which threads were looping, and which loops the threads were in.
The monitor will provide dynamically updated information to the user about which threads were looping, and will furthermore flag two threads as potentially being deadlocked if each were waiting in a condition that involved a variable that the other will touch if it left its loop. The tool will also generate a general warning if a thread continues looping for a sufficiently long time.
Loops could be detected in one of two ways. First, a loop could be detected by inspecting a function's GENERIC tree, which will potentially make the tool language-specific. This will have the advantage of making while conditions obvious. The alternative is to inspect the function's GIMPLE control flow graph for cycles. Although this would be more computationally intensive, it would also be more general because, for instance, the following construct would be handled:
  if(x == 0)
    goto out;

Another challenge would be to obtain accurate information about which variables are still in use after execution leaves a loop. Although it would be possible to enumerate all such variables in the same function, we would need to perform rudimentary inter-procedural analysis to find those of its callees.

5.2  Compiler Extensions

Saved GIMPLE trees   The solution of using an external library to save programmers from writing GIMPLE code (see Section 4) is unsatisfactory because this introduces a compulsory function call at each point where patch code runs. Instead, we propose an API that allows saving a single tree or a list of trees to a file, and loading them for integration into another program. With this system, one could write the patch code in advance and save it to a file, loading it from the file and binding its variables and types at all locations where the code should run.
Instrumentation specification language   In the long term, we strongly believe that the complexity of compilers and the difficulty of programming inside a compiler has been a major factor holding back the development of compiler-assisted instrumentation and verification tools. Consequently, if such applications were made easier to develop, many more instrumentation tools would be created. We share much of this philosophy with the AspectJ toolkit [8]. We intend to implement an AspectJ-like system for GCC, obviating the need to use the GIMPLE API completely.
However, the AspectJ API does not exhaust the possibilities presented by the GIMPLE API. The GIMPLE API allows the instrumentation writer to specify not only additions but also transformations of the source code; for example, GIMPLE can be used to specify loop-unrolling optimizations. Because a system that allows easy implementation of instrumentation plug-ins would also be useful for development of optimizations-just as the reverse is true for the GIMPLE API-we will attempt to make an interface that is both easy-to-use and general.

6  Acknowledgments

Abhishek Rai developed an earlier prototype of the container type detection mechanism and instrumentation for reference-count objects, described in Section 4.3. Yanhong A. Liu and Scott D. Stoller provided valuable feedback to the architectural model, as described in Section 4 and the compiler extensions proposed in Section 5.2.
This work was partially made possible thanks to a Computer Systems Research NSF award (CNS-0509230) and an NSF CAREER award in the Next Generation Software program (EIA-0133589).


T. Ball and S. K. Rajamani. The SLAM toolkit. In CAV '01: Proceedings of the 13th International Conference on Computer Aided Verification, pages 260-264. Springer-Verlag, 2001.
R. Bryant and J. Hawkes. Lockmeter: Highly-informative instrumentation for spin locks in the Linux kernel. In Proceedings of the 4th Annual Linux Showcase and Conference, pages 271-282, Atlanta, GA, October 2000. USENIX Association.
B. Cantrill, M. W. Shapiro, and A. H. Leventhal. Dynamic Instrumentation of Production Systems. In Proceedings of the Annual USENIX Technical Conference, pages 15-28, 2004.
Free Software Foundation. gcov - a Test Coverage Program., December 2005.
The GCC team. GCC online documentation, December 2005.
S. L. Graham, P. B. Kessler, and M. K. McKusick. Gprof: A call graph execution profiler. In Proceedings of the 1982 SIGPLAN symposium on Compiler construction, pages 120-126, June 1982.
R. Grosu, X. Huang, S. Jain, and S. A. Smolka. Open source model checking. In Proceedings of the Workshop on Software Model Checking, Edinborough, Scotland, July 2005. Elsevier.
G. Kiczales, E. Hilsdale, J. Hugunin, M. Kersten, J. Palm, and W. G. Griswold. An Overview of AspectJ. Lecture Notes in Computer Science, 2072:327-355, 2001.
D. Novillo. TreeSSA: A New Optimization Infrastructure for GCC. In Proceedings of the 1st GCC Developers' Summit, Ottawa, Canada, May 2003.
The GDB Project. STABS., 2004.
A. Srivastava and A. Eustace. ATOM: A system for building customized program analysis tools. SIGPLAN Not., 39(4):528-539, 2004.
E. Zadok, S. Callanan, A. Rai, G. Sivathanu, and A. Traeger. Efficient and safe execution of user-level code in the kernel. In Proceedings of the 2005 NSF Next Generation Software Workshop, in conjunction with the 2005 International Parallel and Distributed Processing Symposium (IPDPS 2005), Denver, CO, April 2005.


1Appears in the proceedings of the 2006 NSF Next Generation Software Workshop, in conjunction with the 2006 International Parallel and Distributed Processing Symposium (IPDPS 2006)

File translated from TEX by TTH, version 3.70.
On 22 Jan 2006, 18:53.