A Compositional Account of the Java” Virtual ... - Semantic Scholar

6 downloads 0 Views 1MB Size Report
The Java Virtual Machine (or JVM) is central to the system's aim of providing a secure program execution environment that operates identically on a wide variety ...
A Compositional Account of the Java” Virtual Machine Phillip M. Yelland Sun Microsystems Laboratories 910 San Antonio Road, MS MTV29-117 Palo Alto, CA 94303-4900. +l 650 336 2542

[email protected] The JVM is quite complex, sporting as it does over 200 CISC-style bytecode instructions. Furthermore, the progenitors of the Java programming language sought to expedite program execution by incorporating a verifier into the JVM, to reject errant programs on the basis of a (fairly complicated) static code examination. The complexity of the JVM bedevils the task of producing a description. Perhaps unsurprisingly, therefore, the original narrative account of the JVM published in [LY96] is ambiguous and incorrect in a number of respects [SA98]. On the other hand, the only incontrovertibly accurate description of the architecture - the source code for the JVM interpreter itself - hardly admits ready inspection.

ABSTRACT The Java Virtual Machine (or JVM) is central to the system’s aim of providing a secure program execution environment that operates identically on a wide variety of computing platforms. To be most effective in this role, the JVM needs a rigorous, complete description, to specify precisely the behavior required of implementations. In response, a number of researchers have produced formal accounts of the JVM that seek to define it in an unambiguous and comprehensible manner. Unfortunately, the size and complexity of the JVM means that many of these formal accounts must either restrict their scope substantially, or risk becoming unwieldy and intractable. This paper suggests an alternative approach to the specification of the JVM that seeks to ameliorate such problems by composing together a small set of “microinstructions” to produce the full bytecode set. These microinstructions are encapsulated as functions in the polymorphic functional programming language Haskell, using the familiar mechanisms of Hindley-Milner type inference to characterize the JVM’s rather thorny verifier. In this way, its is hoped that a foundation will be laid for formal descriptions of the Java Virtual Machine that need not trade tractability for completeness.

In order to address these shortcomings, a number of researchers [C97] [FM981 [G97] [Q98] [S97] [SA98] [HT98] have formulated mathematical descriptions of the Java Virtual Machine, yielding valuable insights into its performance. Unfortunately, the size and complexity of the JVM means that most of these accounts must either restrict their ambit to one or two facets of the Virtual Machine or risk becoming unwieldy and intractable.’ This paper propounds a different approach to the formal description of the JVM, resting on the specification of a core “micro-JVM” (the pJVM) that specifies the primitive operations elicited by the bytecodes. The bytecodes themselves are specified by composing together the “microinstructions” furnished by the PJVM. The specification is rendered as a program written in the functional programming language Haskell [P+97], yielding a description that is executable, as well as modular. It is also attractive as regards formal reasoning, since it captures the knottier features of the verifier using the familiar mechanisms of Hindley-Milner type inference.

Keywords Java Virtual Machine, Java bytecode, verification,

Haskell.

1. Introduction The “write once, run anywhere”” claim attached to Sun’s Java technology rests on the abstract computing architecture specified by the Java Virtual Machine (or JVM). The JVM details a secure execution environment for programs encoded as bytecodes that operates identically across the entire spectrum of host platforms. The consistency of implementations on these different platforms is clearly promoted by a precise, accurate description of the JVM. As the scope of the Java application environment expands, and as a growing number of vendors other than Sun produce “clean room” implementations of the system without reference to Sun’s source code, the need for a rigorous specification of the JVM grows increasingly exigent.

The actual account of the Java Virtual Machine presented here is far from comprehensive; a more complete treatment - currently in preparation - is described in [Y99]. The intent of this paper is merely to illuminate the thesis that it is possible to structure a compositional description of the JVM using well-established programming techniques.

’ For a fuller discussion of related work, see section 8.

57

jsr instruction.’ The verifier allows the types of local variables to vary between different invocations of the same subroutine, provided those variables are not accessed for the duration of the subroutine. This is of some convenience to the compiler, though it significantly complicates the verifier see [LY96], [SA98] and [HT98] for more details.

1.1 Overview of the Paper The next section gives an informal sketch of the Java Virtual Machine, highlighting those features that are tricky to characterize formally. A discussion of the l.tJVM follows, starting with its dynamic behavior and continuing with its static semantics. A soundness result shows that for any instruction set (and by extension, program) composed of pJVM microinstructions, type-checking in Haskell corresponds to verification. Then a sample of the Java bytecodes is specified using the microinstructions. Finally, comparisons are drawn with related work, and some indications are given of possible future development.

To preserve the integrity of objects, the verifier a newly created object be initialized prior to Again, proper treatment of object initialization is complicated; indeed, as [FM981 point out, at sion of Sun’s own implementation performed this regard.

2. Highlights of the Java Virtual Machine

3. Preliminaries

2.1 Overall Structure

3.1 Omissions

Most readers are doubtless familiar with the role of the JVM as envisioned by its original designers: Class files - comprising bytecodes and linkage information - are assembled from a variety of sources and passed over a network to the host machine, where they are executed by an implementation of the JVM. Since the JVM has no indisputable information concerning the provenance of the class files it is executing, it must examine the bytecodes therein to ensure that they do not perform illegal operations. To speed execution, almost all existing implementations of the Java Virtual Machine use a verifier to perform the bulk of this examination statically. Given that a class file has passed muster, its execution need not be encumbered by many time-consuming run-time checks.

Paucity of space means that the account of the JVM given in this paper is necessarily simplified. For example, the PJVM described here supports only two primitive data-types (integers and floating-point numbers), rather than the full panoply of primitive types in the real JVM. It only provides for the most basic manipulations of objects (viz. creation, initialization and virtual method invocation) - omitting, for example, static method invocation and direct field access. All methods in this account return integers, whereas in actuality, of course, object references and values of other primitive types may be returned. The specification leaves out the JVM’s exception facility, and classes have been simplified - rather than distinguishing proper classes from interfaces, only classes with multiple inheritance are described. No attempt has been made to give an account of multi-threading capabilities, or of native method invocation. Experience developing the more complete account in [Y99] and related work by Jones [J98] suggest that amending most of these omissions (barring multi-threading) is tedious but not technically infeasible.

2.2 Features of the Architecture The JVM is a stack machine that manipulates data represented by words. A word is an abstract quantity that may represent primitive values of byte, char, short, int or float types, references to objects, code addresses or native pointers. Words are used in pairs to represent values of type long or double. A JVM stuck comprises a collection of frames, each associated with the execution of a single method. A frame in turn consists of two components: a collection of local variables and an operand stuck. Local variables are accessed directly by index, and hold values specific to a given method invocation. An operand stack contains a number of words that are accessed implicitly on a last-in-first-out basis by the JVM bytecodes. Finally, the VM incorporates a heap that contains objects.

2.3 Characteristics

requires that its first use.3 in the verifier least one verincorrectly in

3.2 Haskell Syntax Only a subset of Haskell is used for the definitions in this paper, and some typographic license has been applied (hopefully not too egregiously) in an attempt to render it more widely accessible. Nonetheless, since much of the following discussion centers on a Haskell program, a few explanatory remarks for the benefit of non-aficionados may be found in the appendix.

3.3 Methods and Contexts

of the Verifier

The account centers on the verification and execution of a single method termed the current method.4 It is assumed that the description of the program surrounding the current method is given exogenously by a context. In addition to specifying the class hi-

A majority of the JVM bytecodes have operands and results of specified types. Therefore, the fundamental responsibility of the verifier is to ensure that all the methods in a class file respect the typing constraints of their constituent bytecodes. Two other features of the verifier have proven of particular interest to researchers; its treatment of subroutines (used in the compilation of the try...finally clause of the Java programming language [SA98] [HT98]), and the restrictions it applies to the initialization and use of objects [FM98].

2 Control normally returns from a subroutine by way of the ret instruction to the point of invocation, though abnormal exit is also possible. 3 “Using” an object may involve invoking a method on it or accessing one of its fields.

Subroutines in a method are simply sequences of bytecodes, distinguished in that control passes into them by way of the

4 The current method must not be a constructor, since this would involve further verification restrictions; coverage of such methods is discussed in [Y99].

58

it simply refers to the run-time tive class.6

erarchy of the surrounding program, the context defines the local variables and a number of other entities required by the current method. For this exposition, a particularly simple context has been chosen.

4. Dynamic Semantics of the pJVM

Objects are represented by a Haskell record with three named fields. The field tag contains one of the run-time class identifiers described above, specifying the class of which the object is an instance. The initialized field is used to enforce the restrictions on the use of new objects described in section 2.3. The site field specifies the code position of the object’s allocation, and is bound to a value provided by an instance of the Site datatype - its purpose is explained further in section 5.

4.1 Datatypes

4.1.1

the datatypes underpinning the pJVM are 1. At this stage, apparently superfluous type in some of the declarations; their use should when static semantics are discussed in the

4.1.3

Words

The operand stack is described simply by series of words, nested in pairs, LISP-fashion; the empty stack consists of the trivial tuple “0”. Thus a stack consisting of the three integer words 1, 2 and 3 is denoted “(Wlnt 1, (Wlnt 2, (Wlnt 3,())))“. The values of local variables used in a method are represented by tuples of values. The length of these tuples (and hence the number of local variables available) is fixed for a particular method (in this case, the are three local variables). For each local variable, the context provides a value of type Local that permits retrieval and update of the appropriate element of these tuples (see figure 3 for an example).

In common with elements of the other pJVM datatypes, words are abstract, manipulated only by using microinstructions.5 Figure 2 displays microinstructions for composing and decomposing words containing integers and floating point numbers. There are no microinstructions that provide direct access to words containing code addresses or object references, since they could compromise the integrity of the VM.

The heap has two constituents: a function store mapping references (i.e. integers) to objects, and a “high watermark” (hwm) recording the integer value of the last reference allocated in the heap.7

Classes and Objects

C

Machine State

The yJVM state describes the operand stack, the local variables and the object heap. Each component of the machine state comes with its own type, resulting in the Haskell declaration of VMState shown.

The datatype Word corresponds directly to the JVM’s fundamental data unit. It is defined as a tagged variant, containing integers or floating-point numbers, code addresses, or references to objects. Integers and floats are represented directly by their Haskell equivalents, code addresses by values (actually, function values) of (variable) type k, and references by integers identifying the location of the object referred to in the heap.

4.1.2

of the respec-

The Haskell value representing a method - an instance of the datatype Method - indicates its implementing class and the integer value it returns (recall that in this account, a method simply returns an integer).

The specification of the l_tJVM is divided into two parts: In this section, the intent is to capture the dynamic behavior of the JVM as a series of Haskell functions. The next section shows how the static types of these functions can be arranged to characterize the verifier.

Declarations of given in figure variables appear become evident next section

representation

The illustrative context used in this paper (featured in figure 3) describes four classes in the inheritance hierarchy depicted opposite. Each class is represented by two Haskell values that embody the dynamic and static aspects of the class respectively.

4.1.4

Encapsulating

State Transformations

A straightforward representation of instructions would be as functions mapping one machine state to another. Unfortunately, couching the pJVM specification in this style would make it impossible to use Haskell’s type system to track the use of object references.’ Therefore the ,uJVM follows established practice in the functional programming community by employing a monadic style of presentation [W92] [PJW93]. Actually, the need to account for jumps and subroutine calls and the complex type structure of the microinstructions make it expedient to use a mutable abstract datatype, which Hudak derives from the monadic approach in [H92].

The Haskell data-structure implementing the dynamic behavior of classes - JClassRT - simply defines a collection of constants used to identify the classes in the context. Further definitions (not shown) define an operator “I” on these constants, describing the subtype relationships of the corresponding classes (so that we have Tagn< Taga, for example, but not Tagc I Taga). The data-structure representing the static aspects of each class is JClaas; from the point-of-view of dynamic semantics,

6 Note that the representation of a Java class in the pJVM is unrelated to Haskell’s own “class” construct as described in [P+97]. 7 The pJVM allocates references

’ Technically speaking, the Haskell module defining the pJVM exports no constructors of type Word.

8 See [W92] and [H92] for details.

59

sequentially from 1.

date data data

= Wlnt Int ) WFloat float 1WAddr k 1WRef Int = TagA I Tag6 I Tasc I TagD = Ml&lass JClassRT = MkMethcd (JClass @, n)) Int = MkSie Int = MkObject {tag :: JClassRT, initialized :: BOOI,site :: lnt} = MkVMState {stack :: s, locals :: /, heap :: h) = MkLocalgs = MkHeap {store :: Int -+ Object, hwm :: Int) = MkCont{cfn::VMStateslh-+Int)

Wordtk JClassRT JClass t

data Metiodpn

data data data data data

Site t Object VMStateslh Localgs Heap t

data contslh

Figure 1: Datatypes in the yJVM

inlnt v inFloat v

= Wlnt v = WFloat v

outlnt (Wlnt v) outlnt _

= v = error

outFloat (WFloat v) outFloat _

“Verifv error: Integer word expected”

= v = error

‘Verifv error: Floating-point word expected”

Figure 2: Microinstructions

classA = MkJClass Tag,

composing and decomposing

...

metho& = MkMethod classA 0

...

I%= k&Local (Uv,, &, v2).vo, h(vo, v~, v~) v. (v, v~, v~)) . . . site, = MkSite 1

.

words

- ho,

Cks%, chs~, ClassI,

-Also,

methods, methodc, method0

-Also,

Iv,, Iv*

-Also,

site2, sites

Figure 3: Context definitions

Expression.. .

. . .denotes a continuation

that.. .

result w

Simply returns the word w when invoked.

pushs want

Pushes the word w onto the stack before executing

cont.

popsfn

Pops the value of the stack and passes it to fn before executing

getl I fn

Extracts the value of local variable /, passes it to fn and then executes

set1 I w cod

Sets the value of local variable I to w and then executes

jump

amt mt’

link fn

ant

Transfers control to continuation Forms a word from continuation

cant when invoked;

the continuation

unlink w cant

Extracts a continuation

Allocates an object with site s and class cl and passes a reference executing the resulting continuation.

call m wf%

cmt

conf’ is ignored.

continuation

allocate s d fn

initialize w

60

manipulating

continuation.

cantis ignored. to the new object to function

continuation

to by word w, passing

Figure 4: @VM microinstructions

the resulting

it; continuation

Initializes the object referred to by word wand executes Invokes method m on the object referred the continuation returned.

continuation.

cant

cent and passes it to f?~,executing

from word wand executes

that results.

the resulting

fn,

conf.

the result to function

continuations

fin executing

result w = MkCont (hvms w) pushs w (MkCont cfn) = MkCont (hvms let MkVMState (stack = s} = vrn.s in ch vms(stack t pops fh = MkCont ( hvms. let MkVMState (stack = (w, s)} = vms (MkCont cfn) = fn w in cfh vms(stack = s})

(w, s)})

set1 (Local gfrsfr) w (MkCont cfn) = MkCont (hvms let MkVMState (locals = /} = vrns in crh vms(locals t get1 (Local gtr sfr) fn = MkCont ( let MkVMState (locals = I ) = vrns hVt?JS. (MkCont crb) = fn(gfr/) in cfilvIns)

sfrl w))

jump I k= I link fn cant= th (inAddr cant) update store ref ot$ = htef’. if ref’ == ref then

- Auxiliary function; not public

obj else store ref’

allocate (MkSite o&e) (MkJClass c&g) fn = MkCont ( Avms. let MkVMState( heap = hp) = vms MkHeap (store = s, hwm = hi) = hp newref= hi+ 1 S = update s newref(MkObject (tag = Crag, initialized = False, site = o&e)) (MkCont &I) = fn (WRef newrel) in ch vms( heap = hp( store = s’, hwm = newrel))) initialize (WRef ret) (MkCont ch) = MkCont ( hvms. let MkVMState ( heap = hp) = vms MkHeap (store = s, hwm = hi) = hp in if (refc 0 11ref> h4 then error ‘Yerifv error: Illegal reference” else let obj= sfefin if initialized objthen error “Verifv error: Illegal initialization” else let .4 = update s refobj(initialized = True) in dn vm.s( heap = hp( store t S ))) call (MkMethod (MkJClass imptag) m&t) (WRef ret) fn= MkCont ( hvms. let MkVMState ( heap = MkHeap (store = s, hwm = hi)) = vms in if ( ref < 0 11ref > hi) then error “Verifv error: Illegal reference” else case (s ref) of MkObject ( initialized = True, tag = otag) 1otag < imp&g -+ let (MkCont cfn) = fn (inlnt mrsk) in cfn vms _+ error “Verifv error: Illegal method invocation”) evaluate (MkCont &) = I&I initialstate where initialstate emptyHeap

= MkVMState () (Wlnt 0, Wlnt 0, Wlnt 0) emptyHeap = MkHeap (store = (hi. undefined), hwm = 0)

Figure 5: Dynamic description of microinstructions

Thus instead of exporting the representation of the machine state itself, the pJVM provides a type representing an “encapsulated continuation.” Such a continuation - an instance of type Cant - contains a function (cfn) that maps a machine state to the final answer produced by a computation. To execute an encapsulated continuation, it is passed to the pJVM

instruction evaluate, which applies the enclosed initial machine state.

function to an

4.2 Instructions The majority of the microinstructions provided by the FJVM are functions that manipulate encapsulated continuations.

61

Descriptions of these functions implementations in figure 5.

are given in figure

that allows such relationships to be encoded. This device originated in the work of Remy [R89] dealing with the typechecking of variants and records; the adaptation outlined in [CF91] proves meet for the purposes of the l.tJVM.

4, with

To see how the implementations operate, consider as an example the call instruction, used to invoke a method on an object. The three arguments to this instruction represent - respectively - the method to be invoked, a reference to the object upon which the invocation is to take place and a function that returns a continuation when supplied the result of the invocation. The continuation created in the body of the instruction retrieves the object from the heap, verifies that it is initialized and that the class indicated by its tag field implements or inherits the method. If so, the execution continues with the continuation returned by the application of the function argument to the result of the method (the latter is incorporated into a Word using the inlnt function). Otherwise, an error is reported.

The encoding may be motivated by considering the addition of a number of basic type symbols, c,, . . . , c,,, to the Haskell type system. These symbols represent classes in a hierarchy, partially ordered by a subtype relation, 5. [M91] shows how this subtype relation may be canonically extended to permit subtype judgements between “matching” higher types. RCmy’s device allows type expressions in this enriched language to be encoded in the original Haskell type system so as to reflect this extended subtype relation. Let T and F be two distinct (Haskell) type constants. The encoded representation of each class ci symbol consists of two ci+ = (c,: ,,... ,c:,,)

tuple types, 5. Static Semantics of the pJVM This section shows how types may be assigned the pJVM microinstructions to emulate the actions of the verifier in the JVM.

and c,- = (cl:,,... ,c,,),

where

for llj

Suggest Documents