CS232, Spring 2008

JVM Programming assignment JVM2-1

due Monday, April 21 , at class time

The JVM2

The JVM2 is an enhancement of the JVM1.1 that allows the user to write methods (subroutines).  The JVM2 is constructed by adding three new machine instructions to the JVM1.1.

The three new instructions are the "invokev" ("invoke virtual method", or call a subroutine) and "return" and "ireturn" instructions, with opcodes B6, B1 and AC (hex), respectively.  To understand what each of these instructions do, you first need to understand the contents of the stack when a subroutine is executing.  Each routine (the main routine as well as the subroutines) operates in a stack frame, just as the Wombat5 did.  The JVM stack frame has three parts, as shown in our textbook on pp. 252-253:

  1. space at the bottom for all parameters and local variables (the parameters are just the bottom-most local variables),
  2. space in the middle for the execution environment (explained below),
  3. space at the top for doing the pushing and popping necessary to execute the computations specified by the machine instructions.  This part of the frame is called the "operand stack".

Note that the part of the stack frame forming the operand stack will be shrinking and expanding as operations are performed on the data. The other two parts of the stack frame will remain fixed in size.

The JVM2 uses registers to keep track of the location of each part of the topmost stack frame (that is, the currently active stack frame).  The sp register points to the top of the stack frame and the lv register points to the bottom of the stack frame (the opposite end of the stack frame from where the sp points).

The middle section (the execution environment) is needed to store two values (ordered from bottom to top): the return address, and the value of the lv register for the caller (the routine that called this routine).  The caller’s lv register value needs to be saved so the lv can be restored to its previous value when the current routine returns.

In the real JVM, parameter 0 is a reference to the object on which the method was invoked.  Since we are not dealing with objects, we will use this slot for another purpose, namely to store a pointer to the bottom of the execution environment of the current stack frame.  This pointer will be useful when returning from the subroutine.  As a result, there is no local variable 0, and so local variable 1 will store the first parameter, local variable 2 will store the second parameter, etc.

When a JVM2 routine wants to call a subroutine, the routine must first push space for the (unused) object reference (for example, by using the command iconst_0) and then push the rest of the arguments for the subroutine onto the stack in the order the subroutine expects them.  Then it must execute the invokev instruction.  The 2 bytes following the invokev instruction in the program must be an unsigned 16-bit number (with any value from 0 to 65535) whose value is 2 less than the location of the subroutine in Main memory.  The 2 bytes before the location of the subroutine contain 2 unsigned integers (in the range from 0 to 255). The first integer is the number of parameters, including the unused 0th parameter, and the second integer is the total number of local variables plus parameters.  Every subroutine needs such a set of 2 bytes of data.  The code for the subroutine should immediately follow those 2 bytes in Main memory.

The invokev machine instruction sets aside space for the subroutine's local variables, sets aside space for the execution environment, saves the old lv register value and the return address in the execution environment, updates the lv, sp, and pc registers with their new values, and stores in parameter 0 (the unused parameter that in the real JVM points to the object that was sent the message) the address of the bottom word of the two-word execution environment of the new stack frame.

If the subroutine does not return a value (such as is done by a "void" method in Java) then the return instruction should be used.  The return instruction restores the lv register to its old value, puts the return address in the pc register, and cleans up the stack frame by restoring the sp register to the value it had before the calling routine pushed the parameters on the stack. It also fixes the TOS register to contain a copy of the new top value on the stack.

If the subroutine is supposed to return an integer value to the calling routine (such as is done by a Java method with return type "int"), then the ireturn instruction must be used.  This instruction behaves like the return instruction with the following differences.  The value to be returned is the top-most value in the operand stack, and so the ireturn instruction copies that value into the bottom-most location in the current stack frame (where the 0th parameter was sitting), and then sets the value of the sp register to point to that location.  In this way, when the subroutine returns, the caller will find the value returned by the subroutine sitting on top of its operand stack waiting to be used in further computations.

Note that the invokev instruction sets up the stack frame for the subroutine, and so JVM subroutines, unlike the Wombat subroutines, do not need to finish constructing the stack frame when they first begin execution. Therefore a JVM subroutine can immediately start pushing and popping data to and from the operand stack.

The main program, however, must construct its own stack frame.  It does so by pushing space on the stack for its local variables before doing anything else. The main program has no need for the middle part of a stack frame (the execution environment) and so its stack frame will consist of local variables and the operand stack only, just as it did in the JVM1.

Note also that a subroutine need not explicity dismantle its stack frame before returning, because the return and ireturn instructions dispose of the stack frame for it.  Therefore before returning, a subroutine that returns an integer value needs only to ensure that the value to be returned is on top of its operand stack, and it doesn't need to worry about any other "housecleaning" (or more precisely, "stack frame cleaning") chores.  A subroutine that does not return any value need do nothing special before returning.

Here is a sample JVM2 assembly language program that reads in an integer n and outputs 2*n.  (The code for this program is available in the file JVM2-0.a.)  It calls a subroutine to do the doubling:

n EQU 1      ; n will be local variable 1 for the double routine

;; main program
     iconst_0        ; push parameter 0 for the subroutine call
     input           ; input n & push onto the stack
     invokev double  ; call double
     output          ; output the result from the top of the stack
     stop            ; end the program

; This is the double function.
; Parameters: the unused 0th parameter and an integer n
; It returns 2*n on top of the stack
double:
     .data 1 2  ; # params on stack
     .data 1 2  ; # params + other local variables on stack
     iload n    ;load local variable 1 (that is, the value n)
     bipush 2   ;push the constant 2
     imul       ;multiply 2 and n
     ireturn    ;return 2*n to the caller

Exercise JVM2-1

You can create the JVM2 from the JVM1.1 by adding the invokev,return, and ireturn instructions as defined above.  However, due to the complexity of the microinstruction code necessary to implement these instructions, I have implemented the invokev and the return instructions for you.  All I want you to implement is the ireturn instruction.  A copy of the JVM2 with everything except the implementation of the ireturn instruction can be found here.  You can also see the details of the JVM2 machine in HTML format here.  The implementation of the ireturn instruction will be very similar to the implementation of the return instruction. To help you understand what the ireturn instruction needs to do, here is an annotated list of the microinstructions for the return instruction. As you are implementing the ireturn instruction, remember that the TOS register must contain a copy of the top value on the stack.

Then redo the Wombat exercise W5-2 using the JVM2.  That is, write and test a JVM2 program that reads a non-negative integer n and computes fact(n)--the factorial of n--by using recursion.  Recall that the "contract" for the fact method is that it takes one parameter n on top of the stack and returns fact(n) on top of the stack.  (Note: When you want to call your fact method, you will actually need to push two values onto the stack, since you also need to push space for the unused parameter 0.)  Your main program must look like the following:

   iconst_0     ; push a 0 on the stack for unused param 0
   input        ; input n on stack for factorial's param 1
   invokev fact ; call fact
   output       ; output the result from the top of the stack
   stop         ; end the program

What to hand in

As usual, hand in a hard copy of your "JVM2-1.a" assembly language program . Also, drag a copy of your "JVM2-1.a" program file and your "JVM2.cpu" machine file into a folder called "JVM2-1.your-last-name(s)" and zip up the folder and attach it to an email message to me.