SocketByte
SocketByte

Reputation: 325

Confusing behavior of ObjectWeb ASM and dcmpl/ifgt bytecode instructions

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

Answers (0)

Related Questions