Reputation: 325
I have discovered a strange behavior when playing around with Java bytecode through ObjectWeb ASM. I couldn't find anything about this anywhere else, and I'm quite confused.
I've created a simplified PoC of my issue.
0: ldc2_w #11 // double 100.0d
3: ldc2_w #13 // double 200.0d
6: dcmpl
7: ifle 14
10: iconst_1
11: goto 15
14: iconst_0
15: istore_0
16: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
19: iload_0
20: invokevirtual #26 // Method java/io/PrintStream.println:(I)V
23: return
This set of bytecode instructions are roughly this Java code:
int var1 = 100.0 > 200.0 ? 0 : 1;
System.out.println(var1);
Looks completely normal. But look at this set of instructions:
0: ldc2_w #11 // double 100.0d
3: ldc2_w #13 // double 200.0d
6: dcmpl
7: lstore_0 //
8: nop // These 3 instructions make absolutely no sense.
9: iconst_4 //
10: iconst_0
11: goto 15
14: iconst_1
15: istore_0
16: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
19: iload_0
20: invokevirtual #26 // Method java/io/PrintStream.println:(I)V
23: return
This is the javap dump resulting from simply switching ifle
to ifgt
and negating iconsts. Looks completely mauled. My confusion comes from a simple fact - this actually works. JVM handles it correctly, somehow, even though my decompiler completely stops working and doesn't understand what the hell is going on.
This is the code resulting from decompiling the bytecode above using FernFlower decompiler. It "incorrectly" assumes "var2" is always 0, but this is not the case - I'm actually able to get this bytecode to print "1" (if I switch up the ldc2_w values) which, frankly, makes no sense just by looking at the instructions.
double var3;
int var1 = (var3 = 100.0 - 200.0) == 0.0 ? 0 : (var3 < 0.0 ? -1 : 1);
boolean var10000 = true;
byte var2 = 0;
System.out.println(var2);
If I try to "mimic" this instruction set through ObjectWeb ASM, I correctly get an index out of bounds exception during frame generation (I use automatic frame/maxs calculation). So it's not even "conventionally" possible to generate this bytecode, but somehow JVM understands it.
I can't tell if that's a ObjectWeb ASM bug or a some sort of undefined JVM behavior, or something completely different like javap not correctly showing me the "true" bytecode, which would explain why this code works. I just don't know where to even look to understand this behavior.
This is the entire javap dump: https://pastebin.com/raw/b3L6Q94i
If needed, this is my simplified code to reproduce this behavior using ObjectWeb ASM. @Edit - Provided entire code for easier reproduction.
package test;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
public class Test {
public static void main(String[] args) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES + ClassWriter.COMPUTE_MAXS);
cw.visit(52, ACC_PUBLIC + ACC_STATIC,
"CompilationUnit", null, "java/lang/Object", null);
MethodVisitor mv;
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
}
// Pushing 2 doubles on the stack
mv.visitLdcInsn(200d);
mv.visitLdcInsn(100d);
// Comparing the two doubles using dcmpl
mv.visitInsn(Opcodes.DCMPL);
// Ifgt branching
Label trueLabel = new Label();
Label endLabel = new Label();
mv.visitJumpInsn(Opcodes.IFGT, trueLabel);
mv.visitInsn(Opcodes.ICONST_0);
mv.visitJumpInsn(Opcodes.GOTO, endLabel);
mv.visitLabel(trueLabel);
mv.visitInsn(Opcodes.ICONST_1);
mv.visitLabel(endLabel);
// Storing the result in a variable
mv.visitVarInsn(Opcodes.ISTORE, 0);
// Printing the result
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(Opcodes.ILOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
// Returning
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
cw.visitEnd();
byte[] bytes = cw.toByteArray();
try {
java.io.FileWriter fw = new java.io.FileWriter("CompilationUnit.class");
fw.write(new String(bytes));
fw.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
ProcessBuilder pb = new ProcessBuilder("javap", "-c", "-p", "-s", "-v", "CompilationUnit.class");
pb.redirectErrorStream(true);
try {
Process p = pb.start();
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
p.waitFor();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Upvotes: 0
Views: 80