Jul 7, 2000 - construction of verified compilers because checking the result of the .... oriented representation MIS, a definition and for- ... assembler) code.
Practical Construction of Correct Compiler Implementations by Runtime Result Verification Thilo Gaul Institut f¨ ur Programmstrukturen und Datenorganisation Universit¨at Karlsruhe Zirkel 2, D-76131 Karlsruhe, Germany Wolf Zimmermann Institut f¨ ur Programmstrukturen und Datenorganisation Universit¨at Karlsruhe (see above) Wolfgang Goerigk Institut f¨ ur Informatik und praktische Mathematik Christian-Albrechts-Universit¨at zu Kiel Preußerstr.1–9, D-24105 Kiel 7th July 2000
Abstract
Keywords: Software Quality, Verification, Tools, Compilers, Program-Checking
Software verification is an expensive and tedious job, even software in safety critical applications is tested only. This paper deals with the construction of compilers as an exmaple for big software systems that are hard to verifiy. We describe how program-checking can be used to establish the full correctness of optimizing compilers which may be partly generated by unverified construction tools. We show the practicability of our approach with an application to the construction of verified optimizing compiler back-ends. The basic idea of program-checking is to use an unverified algorithm whose results are checked by a verified component at run time. Run-Time Result Verification in our approach assures formal correctness of the compilation process and its implementation. In our example the approach does not only simplify the construction of verified compilers because checking the result of the transformations is much simpler to verify than the verification of an optimizing code selection. Furthermore, we are still able to use existing compiler generator tools without modifications. This work points out the tasks which still have to be verified and it discusses the flexibility of the approach.
1
Introduction
The use of software controlled systems in the safety critical area forces correctness proofs on the used software. The correctness of the whole system obviously depends on the underlying hardware and the programs running on this hardware. Because the software is written in a higher language - coding machine code directly is too erroneous and costy - the correctness considerations are split into proofs about the high-level language programs, and the correctness of the compiler from programming language to machine code. This paper deals with the correctness of the latter, which is a question as old as the compiler construction area. Though a lot of work is known in the area of correct compilers, we still lack an approach for the construction of practical and fully correct optimizing compilers for real-world programming languages and target machines (see section Related Work ). Correctness of the compiler itself as a piece of software is not needed. Instead we insist on the full correctness of the generated machine programs. This
1 1 The work reported here has been supported by the Deutsche Forschungsgemeinschaft (DFG) in the Verifix
project
1
moves the focus from “compiler correctness” to “compilation correctness”, more precisely spoken we are only interested if the compilation process resulted in the correct target program. One observation while constructing compilers is, that a lot of algorithms - crucial for the correctness of the compiler - are hard to compute, and therefore hard to verify, but their results are often easy to check. Most prominent examples are attribute computations in semantic analysis, code selection and register allocation in the code generation phase. In this paper we propose a checker-based approach to compiler verification, which can be applied if partial correctness of the application is sufficient. Compilers and other transformation systems are usually such partial applications, because in standard programming languages it is very easy to provide a program that forces the compiler to terminate due to memory restrictions. The main focus of this paper is put on implementation verification by program-checking. The basic idea of program-checking we use in this paper is the following: Given a pre-condition P and a post-condition Q for a program or function f (x) we call f partial correct, if (P (x) ∧ defined f (x)) ⇒ Q(x, f (x)) holds. To be able to formulate the postcondition operational we generalize Q to a so called checker Q′ that implies Q. We also allow Q′ to be partial: Q′ (x, y) ⇒ Q(x, y). We define a checked version f ′ of f that delivers the result of f iff the checker Q′ proved the result to be correct, otherwise the result is undefined. For more details on this approach see section 2 and [GGZ98]. In an implementation f ′ is intended either to deliver the result or to emit an error message. This also means, that the resulting application is “more partial” than the original specification of the problem. To achieve a practically useful implementation, the checker itself should be constructed in that way, that it is able to cover all outputs of f . In many cases it is easier to verify the checker Q′ than to verify the generating algorithm f . We will show in our example that this can be up to 1:15, measured in the number of lines to be verified. We will concentrate us on the back-end of an optimizing compiler - the code generation to native machine code using a term-rewrite system. The complete implementation of the code selection, register allocation and operation scheduling is generated with an unverified back-end tool (BEG). For the overall correctness of the compiler system we rely on the verification of the compiler specifications and the verification of the implementation of checker and other parts, that can not be covered by checking.
In this paper we do not deal with hardware verification or verification of the operating system. Though correctness of the base system is essential for the correctness of the whole system, this is beyond the scope of our work.
2
Compiler proach
Verification
Ap-
Starting point for our approach is a complete verified compiling specification which consists of verified transformation rules and global correctness constraints. Figure 1 shows the verificational view on our framework to compiler verification instantiated for one compilation step, i.e. from intermediate language to machine code (in an possible compilation hierarchy with steps from source language → abstract syntax tree → intermediate language → machine language). The correctness notion we use to prove compiler correctness is based on observability of states. A compilation is correct, if it preserves the observable (i.e. input/output) behavior of the source program, in the case of the intermediate representation the behavior of MIS. This gives us the freedom to compile programs not on an one-to-one basis, but to transform programs optimized. For a general discussion about the Verifix compiler correctness approach see [GZG+ 98, ZG97]. In figure 1 the generator tool stays “outside” the verification process, because it does not matter, how we generated the implementation of the corresponding unverified parts. We subdivide the implementation into parts that still have to be verified, which include the programchecking parts, and into parts, that can be used unverified or generated with unverified tools: verified parts... • ...implement the checker check π, and have to be verified against it • ...have to be implemented verified on machine level, i.e. compiled with a verified compiler • ...perform interaction of several checkers, form “outer” interface of the whole system • ...assure functional view on unverified parts by wrapping them unverified parts...
Implementation Language Level
Specification Level
Semantics Q
Machine Implementation Level
concr. Prog. Q
abstr. Prog. Q
concr. Prog. Q
Check Check’
Compiling Specifiation
f
f’ Compilation
Semantics Z
concr. Prog. Z
abstr. Prog. Z
concr. Prog. Z
Generator Figure 1: Verification Architecture Back-End
• ...are the code pieces to be checked • ...implement π unverified - in any language • ...maybe generated from unverified tools
3
Case Study Verified Compiler Backend
One of the most error-prone parts of a compiler is the back-end which produces the machine code. In this transformation from machine independent intermediate language to native machine code many complex optimizations are performed and optimizing transformation techniques are used. For the construction of optimizing code selectors several subclasses of cost controlled term-rewriting systems are known. Well established techniques are the classes of tree-transducers[ESL89, AG85] and bottom-uprewrite systems (BURS)[NK96, Pro95]. In this paper we will put the focus to those code selection techniques.
Implementations of such cost controlled rewrite systems use complex algorithms and heuristics, because - depending on the class of the rewrite system - problems are in NP. Additionally, for a practical compiler we need register allocators and instruction schedulers, which are usually integrated into the generator that produces the code selector implementation from specifications. In order to prove the practicability of our approach we used the widely accepted - and industrial used - back-end generator tool BEG as the core generator of our back-end framework. One has to keep in mind, that this tool can only generate unverified implementations that have to be checked. Main reason is, that such tools - especially BEG - generate very huge tricky and hard to verify C implementations, and additionally we also do not have access to the sources of the tool. Our intermediate language is the basic-block-graph oriented representation MIS, a definition and formal Abstract State Machine (ASM2 ) semantics can 2 An
introduction to Abstract State Machines (ASM) can
Code Select. Specification
BEG (generates)
Inner Part BBG MIS
Code Selection
ABBG
Register Allocation Instr. Schedul.
ABBG Checker
Transformation OK!
BBG
DECAlpha
Check-Error!
Figure 2: Architecture Checked Back-End
be found in [GHHZ96]. The target we chose for this example is the DEC-Alpha architecture, a highperformance RISC workstation processor family. A formal ASM based semantics definition suitable for the purposes of back-end verification can be found in [Gau95]. All our proofs are done w.r.t. these ASM based semantics of intermediate and target language. Starting point is a complete verified compiling specification which consists of verified transformation rules and correctness constraints for the global rewrite mechanism, register allocation and the scheduling of program trees. Precise definitions and proofs are given in [ZG97]. The checker verifies those constraints at runtime of the compiler and therefore assures the correctness of the compilation process. The back-end tool BEG generates the code selector from a set of term-rewrite rules annotated with costs and mappings to target machine code. The implementation consists of a tree pattern match phase, that tries to find a cost minimal cover of the program tree w.r.t. the rewrite rules. Afterwards register allocation and code emitting traversals are initiated. In [ZG97] we showed how to decompose the overall correctness of such a rewrite system into local correctness of single term-rewrite rules and global correctness aspects, that deal with the rewrite process, scheduling and register allocation. We use this decomposition to define verified and unverified parts of the back-end and to formulate the correctness requirements to the checkers. We are able to encapsulate the complete code selection part with register allocation and scheduling generated by the back-end tool into an unverified part. Figure 2 shows the architecture of the checked backend. Input to the unverified - BEG generated - part be found in [Gur95]
is the basic block graph (BBG), output is the annotation graph with rewriting attributes (ABBG). The BBG is annotated with the required rewrite information while the unverified process runs. We do not have to know how this annotation is performed3 , we are only interested in the output. Additionally it does not matter if the BBG is not longer in original state, because the checker only uses the attributes. The checker rejects a concrete annotation (ABBG) at runtime with an error message or passes the graph to the transformation phase, which finally performs the rewrite sequence and emits the target (DEC-Alpha assembler) code. Attributes of ABBG nodes are the rules to be applied, the allocated registers and an order in the schedule in which the tree must be evaluated. The transformation phase has to be verified, but is a very simple task now, because it does not have to search for a possible - cost optimal - rewriting, it only performs the transformation. The result is native machine code in assembler format, therefore we still rely on a verified assembler/linker to achieve a complete verified compiler tool chain.
3.1
Measurements
We applied our approach to a back-end selecting code for DEC-Alpha. Our implementation language for the verified parts is Sather-K, a type-safe objectoriented language [Goo97]. The code generator generator BEG produces a C implementation with 18.000 lines of code, the generator itself tool is written in 35.000 lines of Modula-2 code. Table 1 compares lines of code and program sizes of our example. If we would be forced to verify the implementation 3 In practices we do of course know what the generated parts do, but we are not sure about their correctness.
Modula2/Sather-K Lines Byte Generator BEG (Modula2) Generated C Code
Binary Prog. Byte
35.000
2 MB
1.1 MB
18.000
400 KB
500 KB
Impl. MIS Code-Selection Verified Parts (Sather-K)
500 (Reader) +300 (Checker) +400 (Transform)
Industrial: ANDF ⇒ ST9
140.000
200 KB 6.4 MB
3.5 MB
Table 1: Lines of program code to verify for example back-end traditionally, we would either have to verify the generated C code or the generator implementation4 . Applying the checker approach we can use the generated C code unverified, but have to verify the additional verified parts like interface (reader), checker and transformation code. If we compare the number of lines to verify on implementation level as an indicator of the expense of verification, we obtain a factor of 15 between the generated C code and the implementation of the additional verified parts. This shows the feasibility of the program-checking approach, even if we consider that different programs can be very different in the level of difficulty to verify. The last line of Table 1 shows the size of an industrial code selector generated with BEG (ANDF⇒ST9). Checker and transformation part for this application would be also much bigger, but the relation between unverified and verified code will be even worse.
4
Related Work
Our checker approach is closely related to the work of M. Blum on result-checking [BK95, WB97] and the ideas of [GG75]. A more detailed discussion of the theoretical aspects of our approach can be found in [GGZ98]. Program checking is already used in compiler construction for checking properties necessary to establish correctness of a transformation. Necula and Lee [NL98] describe a compiler which contains a certifier that automatically checks the type safety and the memory safety of any assembler program produced by the compiler. The certification process detects 4 We would even have to use a verified C compiler, which is usually not available
statically compilation errors of a certain kind but it does not establish full correctness of the compilation. Nevertheless, this work shows that program checking can be used to produce efficient implementations with consideration of safety requirements. [PSS98] apply the idea of program checking for translation of finite state automata. They consider reactive systems. However, their assumptions lead to programs which are single loops with a body that implements a function from inputs to outputs. Their approach checks the loop body. Since compilers as well as the different modules implement functions and every compiler refuses almost every program, we can apply program checking for the construction of correct compilers.
5
Conclusion
We addressed the problem of compiler verification for real-world compilers and languages with the focus on the code generation phase, and presented a concrete back-end verification framework. Our approach emphasizes the software engineering aspect, because it bridges the gap between the verification of such a complex software system and its practical implementation, especially with modern generators. The main idea is to assure correctness of the implementation by introducing runtime programcheckers that check the result of the complex code generation phase. Measurements in our case-study (see Table 1) show the practicability of our approach. Acknowledgements This work is supported by the Deutsche Forschungsgemeinschaft project Go 323/31 Verifix (Construction of Correct Compilers). We are grateful to our colleagues in Verifix.
References [AG85]
Alfred V. Aho and Mahadevan Ganapathi. Efficient tree pattern matching: an aid to code generation. In Brian K. Reid, editor, Conference Record of the 12th Annual ACM Symposium on Principles of Programming Languages, page 334, New Orleans, LS, January 1985. ACM Press.
[BK95]
Manuel Blum and Sampath Kannan. Designing programs that check their work. Journal of the Association for Computing Machinery, 42(1):269–291, January 1995.
[ESL89]
H. Emmelmann, F.-W. Schr¨oer, and R. Landwehr. Beg - a generator for efficient back ends. In ACM Proceedings of the Sigplan Conference on Programming Language Design and Implementation, June 1989.
[Gau95]
T.S. Gaul. An Abstract State Machine Specification of the DEC-Alpha Processor Family. Verifix Working Paper [Verifix/UKA/4], University of Karlsruhe, 1995.
[GG75]
J.B. Goodenough and S.L. Gerhart. Toward a Theory of Test Data Selection. SIGPLAN Notices, 10(6):493–510, June 1975.
[GGZ98]
W. Goerigk, T.S. Gaul, and W. Zimmermann. Correct Programs without Proof? On Checker-Based Program Verification. In Proceedings ATOOLS’98 Workshop on “Tool Support for System Specification, Development, and Verification”, Advances in Computing Science, pages 108 – 122, Wien, New York, 1998. Springer Verlag.
[GHHZ96] T.S. Gaul, A. Heberle, D. Heuzeroth, and W. Zimmermann. An ASM Specification of the Operational Semantics of MIS. Verifix Working Paper [Verifix/UKA/3 revised], University of Karlsruhe, 1996. [Goo97]
Gerhard Goos. Sather-K — The Language. Software — Concepts and Tools, 18:91–109, 1997.
[Gur95]
Y. Gurevich. Evolving Algebras: Lipari Guide. In E. B¨orger, editor, Specification
and Validation Methods. Oxford University Press, 1995. [GZG+ 98] W. Goerigk, W. Zimmermann, T. Gaul, A. Heberle, and U. Hoffmann. Praktikable konstruktion korrekter u ¨ bersetzer. In Softwaretechnik ’98, volume 18 of Softwaretechnik-Trends, pages 26–33. GI, 1998. [NK96]
Albert Nymeyer and Joost-Pieter Katoen. Code Generation based on formal BURS theory and heuristic search. Technical report inf 95-42, University of Twente, 1996.
[NL98]
G. C. Necula and P. Lee. The design and implementation of a certifying compiler. In Proceedings of the 1998 ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), pages 333–344, 1998.
[Pro95]
Todd A. Proebsting. BURS automata generation. ACM Transactions on Programming Languages and Systems, 17(3):461–486, May 1995.
[PSS98]
A. Pnueli, O. Shtrichman, and M. Siegel. Translation validation for synchronous languages. Lecture Notes in Computer Science, 1443:235–250, 1998.
[WB97]
Hal Wasserman and Manuel Blum. Software reliability via run-time resultchecking. Journal of the ACM, 44(6):826– 849, November 1997.
[ZG97]
W. Zimmermann and T. Gaul. On the Construction of Correct Compiler BackEnds: An ASM Approach. Journal of Universal Computer Science, 3(5):504– 567, 1997.