Debugging

Inspector

The Inspector is the tool co-developed with the VM for debugging. Launching the Inspector is as simple as using the mx inspect command.

mx inspect -cp com.oracle.max.tests/bin test.output.GCTest2

The Inspector window should appear in a few moments. Go here for other ways of launching the Inspector.

Testing Maxine

Benchmarks

The most useful way to test Maxine is to execute some of the standard benchmarks on the image previously built with the mx image command. In this example, we will use the DaCapo benchmarks. After downloading the benchmark suite from https://sourceforge.net/projects/dacapobench/files/9.12-bach-MR1/dacapo-9.12-MR1-bach.jar, set the following environment variable:

export DACAPOBACH_JAR=/Users/acme/benchmarks/dacapo-9.12-MR1-bach.jar

Then execute the following command:

mx testme -insitu -tests=dacapo
----------------------------------------------------------------------------------------
Running reference       DaCapo-bach avrora:                    3960 ms
Running maxvm (std)     DaCapo-bach avrora:                   13815 ms           3.488x
----------------------------------------------------------------------------------------
...

Note that the harness used to run the benchmarks against a reference VM is a little brittle. In particular, it compares the output written to stdout by the two executions and if they don’t match, it determines the Maxine VM execution to have failed. We’ve noticed that for some benchmarks on some platforms, the execution output is not such a reliable fingerprint. To see the actual output of the benchmark, and the exact command used to run it, look in the maxine-tester/insitu directory.

Regression testing

Maxine includes a set of tests that are useful in catching regressions and measuring progress when making changes. The three types of tests included in the distribution are described below.

JUnit tests

Tests the Maxine code base prior to building and running the VM. For example, there are JUnit tests for the general purpose utility classes in the Base project. There are also JUnit tests that use the various IR interpreters to ensure that each level of IR in Maxine’s compiler produces the correct output.

VM tests

These are tests that are executed on the VM. The first subcategory of VM tests are very simple unit tests that test a specific VM feature and/or Java bytecode instruction in isolation (i.e. avoiding as many other VM features as possible). To ensure strong isolation, these tests are built into the boot image and executed in such a way that precludes using a more general testing framework such as JUnit. The second subcategory of VM tests are called output tests. These tests are comprised of standard Java programs (i.e. they have a class containing a main method) that produce some deterministic output via System.out. These tests compare the output of these programs when run under Maxine VM and another trustworthy JVM (such as HotSpot).

Cross-ISA tests

When porting Maxine VM’s compilers to a new Instruction Set Architecture QEMU is utilized to virtually run unit tests and regress the correctness of the generated code. To be able to run cross-ISA tests Maxine VM relies on gcc cross-compilers, gdb-multilib and qemu. The easiest way to run the cross-ISA tests is by using the beehivelab/maxine-dev docker image.

If you insist on running natively, assuming an Ubuntu 18.04 LTS installation, the following will install the required packages.

ARMv7 and AArch64
sudo apt-get install \
    gdb-multilib qemu-system-arm \
    gcc-aarch64-linux-gnu gcc-arm-none-eabi
RISC-V

GCC:

sudo apt-get install gcc-riscv64-linux-gnu

GDB:

sudo apt-get install wget texinfo
mkdir /tmp/riscv
cd /tmp/riscv/
wget https://ftp.gnu.org/gnu/gdb/gdb-8.2.1.tar.xz
tar xf gdb-8.2.1.tar.xz
cd /tmp/riscv/gdb-8.2.1
./configure --target=riscv32-elf,riscv64-elf --disable-multilib --prefix=/opt/riscv
make -j
sudo make install

QEMU:

sudo apt-get install libglib2.0-dev libpixman-1-dev flex bison
wget https://download.qemu.org/qemu-3.1.0.tar.xz
tar xf qemu-3.1.0.tar.xz
cd /tmp/riscv/qemu-3.1.0
./configure --target-list=riscv64-softmmu,riscv32-softmmu,riscv64-linux-user,riscv32-linux-user --prefix=/opt/riscv
make -j
sudo make install

Don’t forget to add /opt/riscv to PATH.

Logging and Tracing

Maxine provides two related mechanisms for logging and/or tracing the behavior of the VM, manual string-based logging using the com.sun.max.vm.Log class, or more automated, type-based logging, that is integrated with the Inspector, using com.sun.max.vm.log.VMLogger. These are related in that VMLogger includes string based logging as an option and so can replace the use of Log. Currently the VM uses a mixture of these two mechanisms, with conversion being done opportunistically. For simplicity, we will use the term tracing to describe string-based logging in the following. If you are adding logging to a VM component you are strongly encouraged to use the VMLogger approach.

Manual Tracing

Use the class com.sun.max.vm.Log to do manual tracing. The class includes a variety of methods for printing objects of various types. By default the output goes to the standard output but can be re-directed to a file by setting the environment variable MAXINE_LOG_FILE before running the VM. To selectively enable specific tracing in the VM, define a com.sun.max.vm.VMOption with the name -XX:+TraceXXX, where XXX identifies the tracing.

You should avoid string concatenation (or any other code involving allocation) in tracing code, especially inside a VmOperation. While this should not break the VM (allocation will fail fast with an error message if a VM operation does not allow it), allocation can add noise to your logs. Lastly, if the logging sequence involves more than one logging statement, you should bound the sequence with this pattern:

boolean lockDisabledSafepoints = Log.lock();
// multiple calls to Log.print...() methods
Log.unlock(lockDisabledSafepoints);

This will serialize logging performed by multiple threads. Of course, it will also serialize the execution of the VM and may well make the race you are trying to debug disappear!

Native Code Tracing

Maxine provides some tracing of the small amount of native code that supports the VM. By default this is conditionally compiled out of the VM image but can be selectively enabled by editing com.oracle.max.vm.native/share/log.h and rebuilding with mx build and rebuilding the VM image. This is particularly useful if the the VM crashes during startup. For example to enable all tracing set log-ALL to 1.

Type-based Logging

In type-based logging, the actual values that you want to log are passed to an instance of the com.sun.max.vm.log.VMLogger class using methods defined in the class. Evidently, at the VMLogger level, type-based logging is something of a misnomer, as it cannot know the types of the actual values. In practice the values are logged as untyped Word values, but extensive automated support is provided to handle the conversion to/from Word types. The optional tracing support is driven from the values in the log. For more details see Type-based Logging.

Debugging Maxine Java Tasks

The Maxine project includes a number of Java programs that can be launched as commands of the mx script. For example, the mx image command described above runs the com.sun.max.vm.prototype.BootImageGenerator class on a host JVM. This simplest way to debug such a command is to use the -d global option of the mx script. This will launch the Java program with extra options telling it to wait and listen for a JDWP-capable debugger on port 8000. You then configure a JDWP-enabled debugger to attach to this port.

The advantage of this approach is that you can easily launch the command with different command line arguments without having to create/modify an IDE launch configuration.

Core dump

To get a core dump from a Maxine VM process, it is simplest to do gcore <pid> from another shell. This forces a core dump but does not terminate the process, which continues after the dump is taken. An alternative is to use kill -s ABRT <pid> which does kill the process after the dump is taken. One other difference is that gcore allows the path to the core dump file to be specified with the -c <corefile> option, whereas kill puts it in a default location, typically core in the current working directory.

It is possible to force a core dump on a fatal VM error by setting the option -XX:+CoreOnError when running the VM.

The following invocation:

mx inspect --mode=attach --target=file --location=dumpfile

will then bring up the Inspector on the core dump. If you omit the --location argument, it will put up a dialog box.

Unfortunately this will only work if the associated Maxine VM was run with the -XX:+MakeInspectable option, otherwise some key data structures needed by the Inspector will not have been created.