Wednesday, December 17, 2008

The Maxine Inspector: First Contact

Once everything is up and running, things are ready to become really interesting. This article is about the Maxine Inspector, the powerful debugging tool that makes the entire Maxine project - in my opinion - one of the most interesting VM research projects these days.

The Inspector supports the close observation of an application running on Maxine, including the VM itself. This goes as far as being able to inspect the values currently stored in processor registers. All code that was compiled from Java source code, i.e., also the Maxine sources, can be inspected in native code and Java bytecode representation. Access to the source code representation is currently not supported.

I've only just started exploring the things that are possible with the Inspector. The Java application I've been using is the Hello World application known from my previous article; the one that comes with Maxine. As a side note, I just discovered today that it is not necessary to use the command line bin/max vm -cp VM/bin util.HelloWorld to run Hello World; apparently, the max script is capable of nicely doing that for us:

~/workspace/maxine~maxine$ bin/max helloworld

Anyway. On to the really interesting things.

Exploring the Inspector

Running an application in the Inspector is as simple as invoking it via the max script, only using the inspect command instead of vm, which was used to simply run the application:

~/workspace/maxine~maxine$ bin/max inspect -cp VM/bin util.HelloWorld

It takes a while and dumps lots of information on the screen, but eventually the (large) window shown in the image below appears. It really makes you wish you had two extra-large screens with exorbitant resolution available.

From left to right, and top to bottom, the Inspector GUI has the following elements:
  • Threads: all currently existing threads in the Maxine instance being observed.
  • Registers: the processor register values at this moment in time in the current thread.
  • Stacks: all threads' call stacks, with all the methods that are on the stacks right now.
  • Methods: the (native or byte) code of selected Java methods.
  • Breakpoints: a list of breakpoints that can be activated and deactivated.
  • Thread Locals: thread-local state.
I obviously don't know about the details of all of them (yet).

At this point in time, the Inspector shows the Maxine VM at the earliest possible point at which it can be observed: at the entry of the first Java method that is ever entered once the boot image has been loaded into memory and the boot image loader has jumped into the image. The Methods window contains a tab named[0]; this indicates that the method at whose start the Inspector is right now is Below the tab, the entire signature of the method is given: int run(Pointer, Pointer, Pointer, Word, Word, int, Pointer)[0].

Of course, the first success for an Inspector novice like me is to successfully run the inspected application as if nothing special was going on. This can be achieved by clicking the Resume button (which looks like the small image to the right) in the Methods window. I had to click it multiple times until the menu bar turned red, indicating termination of the VM. After closing the Inspector and looking at the console again, I discovered the Hello World! message somewhere in between the lots of output generated by the Inspector.

Inspecting Hello World

Let's do something real this time. After starting the Inspector again (see above), I want it to run all the VM initialisation code and come back to me once HelloWorld.main() is entered. Since I want to see both the machine code and the Java bytecodes of the method, I need to activate the bytecode view. This is done by clicking the black triangle icon left to the run() method's signature in the Methods window and selecting the Display Bytecodes option. Now, the two representations will appear side by side. The triangle icon opens context menus in all Inspector windows.

Next, I set a breakpoint at the beginning of HelloWorld.main(). I do this by opening the context menu for the Breakpoints window and selecting Break at Method Entry->Method on classpath, by name....

A dialog box appears. In the text input field at its top, I enter the name of the class (HelloWorld) and select the correct one from the list that appears; it is util.HelloWorld. Once the selection is confirmed by double-clicking the class name or clicking the Select button, another dialog box appears that lists all the available methods. Here, I select main(). Once that is done, the breakpoint is registered and activated and appears in the Breakpoints window.

The Inspector can now be told to continue execution until it reaches the breakpoint by (in my case, twice) clicking the Resume button. After the second click, some window borders flash red, and the Inspector window finally comes to a halt, looking a bit more full than before. Several things are interesting about the new scenario.


This window now shows four different threads, one of which (no. 2 with VM ID 0) is the main thread. Another is the primordial thread, which is, upon closer observation, the only thread that was running when the Inspector window appeared for the first time. The two remaining threads are for the garbage collector and Maxine's JIT compiler.


There are now four different tabs in the Registers window, one for each currently existing thread. The register set for the main thread is being shown, which makes sense as the breakpoint we have reached pertains to this thread. Clicking on the other tabs reveals the different register values for the other threads and also updates the Stacks and Thread Local windows as the thread being observed changes.


The current call stack is a lot more full than at startup time, and we can see that HelloWorld.main(), the top method, has been invoked reflectively through Method.invoke(). Clicking on the latter will update several of the windows accordingly, but that is out of scope now. We can also see that the invocation of HelloWorld.main() was eventually reached from an activation of, which was apparently invoked from a native method that cannot be inspected (just click on its name and you'll see). In fact, the methods from to JavaRunScheme.lookupAndInvokeMain() are all Maxine methods, which can be seen from the tool tips that appear when the mouse pointer hovers over their names a while: their fully qualified names are given on the far right of the tool tip.

Thread Locals

This window now also has four tabs, one for each thread, and the values of the thread-local data can be inspected.


Finally, we come to the most interesting window. It has also one more tab, namely for the method HelloWorld.main(). It is immediately apparent that the Inspector has reached a breakpoint: it is highlighted by a yellow box. The red arrow indicates the current instruction pointer, and it can be seen which machine code instructions correspond to what bytecode instruction. Apparently, the method prologue has already been executed, and the first instruction corresponding to an actual Java bytecode instruction in main() is about to be executed.

Now, I want to see what a String object looks like; and I know I can easily get hold of it once it has been put on the stack by the LDC bytecode instruction (the second in main()). So I click on the INVOKEVIRTUAL instruction (note how the machine code view is adjusted) and then on the Run to selected instruction (ignoring breakpoints) button above the bytecode instructions (see right for an image). (I had to select-and-click twice to reach the INVOKEVIRTUAL instruction.)

Looking at the machine code generated for the LDC bytecode instruction, it seems the pointer to the String object was stored in the RDI register of my Pentium 4 CPU. Letting the mouse pointer hover over the green rdi text will yield a tool tip showing that there is indeed a String there.

And this is when we get to one of the coolest features of the Maxine Inspector. It is truly capable of (in the Smalltalk sense) inspecting objects. Clicking on the green rdi text (or on the green value stored in the RDI register as visible in the Registers window) yields a window displaying various details on the String object we're after.

Here are some of the details:
  • Object layout. The numbers at the left-hand side of the window indicate at which offset from the object pointer the corresponding values are stored. From this window, we can learn that the first word in the object is a pointer to another object representing the inspected object's class. (These are called hubs in Maxine, and clicking on the green text will yield an inspector for the hub, which displays, amongst other things, the entries of the class's virtual method table. But that's another story. For now, just remember that everything green indicates an object that can be inspected.) The second word in the String seems to be related to synchronisation, the third points to an array of characters representing the String, and so forth.
  • Slot names. It is incredibly useful to be able to reason about object contents in terms of the names the values are stored with, instead of using slot numbers and offsets.
  • Slot contents. The values of all of the object's slots are visible. As mentioned above, clicking on the green entries will open another inspector (again, green means object).
The context menu of the inspector (black triangle) contains several options for controlling what is displayed, and how. For example, it is possible to show the concrete heaps in which the different values are stored.


This shall be it for now. I am totally baffled with the things one can do with the Inspector, and I wish I had had something like this much earlier. During my doctoral research, I was also working with a JVM implemented in Java (Jikes), and did not have any such support. The question remains whether it is possible to port at least some of the Maxine Inspector functionality over to Jikes, to make this great research VM as debuggable as Maxine.

No comments:

Post a Comment