We implemented a Java bytecode generator that creates random class files, which we also tested on GraalVM.
Over the last couple of weeks we implemented a test suite that resembles
CompileTheWorld from the Graal compiler test suite. Utilizing the compiler API, we repeatedly generate random class files and load them using a custom class loader. Then we run those files with the HotSpot interpreter and compare this result to a run with the Graal compiler.
In a number of test runs we managed to find a bug that seems to stem from field default values
as well as the java.lang.Math.scalb method. As the generated class files are usually fairly complex,
we narrowed it down to the following class file:
public class Bug
minor version: 0
major version: 52
flags: ACC_PUBLIC
Constant pool:
#1 = Utf8 Bug
#2 = Class #1 // Bug
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 main
#8 = Utf8 ([Ljava/lang/String;)V
#9 = NameAndType #5:#6 // a:I
#10 = Fieldref #2.#9 // Bug.a:I
#11 = Utf8 java/lang/System
#12 = Class #11 // java/lang/System
#13 = Utf8 out
#14 = Utf8 Ljava/io/PrintStream;
#15 = NameAndType #13:#14 // out:Ljava/io/PrintStream;
#16 = Fieldref #12.#15 // java/lang/System.out:Ljava/io/PrintStream;
#17 = Utf8 java/lang/Math
#18 = Class #17 // java/lang/Math
#19 = Utf8 scalb
#20 = Utf8 (FI)F
#21 = NameAndType #19:#20 // scalb:(FI)F
#22 = Methodref #18.#21 // java/lang/Math.scalb:(FI)F
#23 = Utf8 java/io/PrintStream
#24 = Class #23 // java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (F)V
#27 = NameAndType #25:#26 // println:(F)V
#28 = Methodref #24.#27 // java/io/PrintStream.println:(F)V
#29 = Utf8 Code
#30 = Utf8 StackMapTable
{
static int a;
descriptor: I
flags: ACC_STATIC
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: sipush -279
3: istore_1
4: getstatic #10 // Field a:I
7: ifle 21
10: getstatic #10 // Field a:I
13: ifge 17
16: nop
17: iconst_1
18: goto 22
21: iconst_0
22: ifeq 27
25: iconst_0
26: istore_1
27: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
30: iconst_m1
31: i2f
32: iload_1
33: invokestatic #22 // Method java/lang/Math.scalb:(FI)F
36: invokevirtual #28 // Method java/io/PrintStream.println:(F)V
39: return
StackMapTable: number_of_entries = 4
frame_type = 252 /* append */
offset_delta = 17
locals = [ int ]
frame_type = 3 /* same */
frame_type = 64 /* same_locals_1_stack_item */
stack = [ int ]
frame_type = 4 /* same */
}
When run with the interpreter, this results in the output of -0.0 while execution of code compiled by Graal yields -1.0.
We could not further minimize the bytecode. We managed to recreate the class in ASM to perform manual corrections, but even the removal of the nop (bci 16) results in the test executing correctly.
Just for comparison, the decompiled source code would be something like:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class Bug {
static int a;
public static void main(String[] var0) {
short var1 = -279;
boolean var10000;
if (a > 0) {
if (a < 0) {
;
}
var10000 = true;
} else {
var10000 = false;
}
if (var10000) {
var1 = 0;
}
System.out.println(Math.scalb((float)-1, var1));
}
}
Interestingly, this source code also does not reproduce the bug when compiling it again.
We used the following version of the LabsJDK on a Fedora release 28 machine:
java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12-jvmci-0.53, mixed mode)
Hi @skloibi, thanks for the report, could you please post your Bug.class ?
Hi, thank you for the fast response.
Yeah sure, sorry that I forgot that in the original report: IncorrectResultsForMathScalb.class
If it is useful, I also uploaded the ASM generator that reproduces the exact same file: IncorrectResultsForMathScalb.java
I can't reproduce it, could you please post detailed steps on how to reproduce, if possible including the test harness you are using. I've tried warming it up, forcing compilation... without success.
Okay, the steps that we perform in our test suite are as follows:
We generate the class file (or use an existing like the provided one) and load it using a custom class loader.
Then we get the "expected" result by invoking the "main" Method of the class via reflection:
Class<?> generatedClass = ...;
generatedClass.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
The result is simply all the content that is written to STDOUT by the class (the generator produces print statements).
We then invalidate the class (and the class loader - to not influence consecutive runs) and reload it.
We compile each method of the class using a stripped down variant of the CompileTheWorld code in https://github.com/oracle/graal/blob/master/compiler/src/org.graalvm.compiler.hotspot.test/src/org/graalvm/compiler/hotspot/test/CompileTheWorld.java#L591-L631. It uses the same functions to get the InstalledCode but skips some of the metrics as they are not needed for our purpose and also compiles only this class.
Execution is done by running InstalledCode#executeVarargs on the compiled main method.
I have refactored the test suite and uploaded it: Graal Generator Tests.
You can build it by running mx build on the cloned repository and run it with mx graal_generator_tests <classfile> - e.g. mx graal_generator_tests IncorrectResultsForMathScalb.class.
Thanks for the detailed reproducer!
Your test uncovered rogue canonicalizations in IfNode. The bug is completely unrelated to Math.scalb, it triggers the right pattern at the right moment (cold method). A fix with some additional improvements is already up in our internal system.