javakotlinjvmbytecodejava-bytecode-asm

How to use visitLocalVariable in ASM correctly?


So I am trying to add information about locals I use. The current way I am trying to achieve it - is:

methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "something", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitInsn(ICONST_1);
Label label1 = new Label();
methodVisitor.visitJumpInsn(IFEQ, label1);
methodVisitor.visitLdcInsn("number");
methodVisitor.visitVarInsn(ASTORE, 0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESTATIC, "org/bezsahara/minikotlin/NamingTests", "accept", "(Ljava/lang/Object;)V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitLabel(label1);
methodVisitor.visitIntInsn(BIPUSH, 21);
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
methodVisitor.visitVarInsn(ASTORE, 0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESTATIC, "org/bezsahara/minikotlin/NamingTests", "accept", "(Ljava/lang/Object;)V", false);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("hello", "Ljava/lang/String;", null, label0, label1, 0);
methodVisitor.visitLocalVariable("goodbye", "Ljava/lang/Integer;", null, label1, label2, 0);
methodVisitor.visitMaxs(0, 0);
methodVisitor.visitEnd();

However, when using Intellij decompiler (I think it uses FernFlower). Variable names are incorrect:

public static void something() {
    if (true) {
        String goodbye = "number";
        NamingTests.accept(goodbye);
    } else {
        Integer hello = 21;
        NamingTests.accept(hello);
    }
}

After examining compiler outputs. It seems they indicate only LOAD kind of operations on variables. But it seems unreasonable. I mean if I just assign a variable to something it will loose all naming information then.

So, I would appreciate it if you can help me understand:

  1. Only LOAD should be marked in visitLocalVariable?
  2. If so, why is it reasonable? Why can't I also mark STORE?

Solution

  • I don't know enough about the decompiler's implementation details to tell you exactly why it produced that output, but the bytecode you generated is quite odd in the first place.

    As you may know, visitLocalVariable corresponds to adding entries to the LocalVariableTable attribute of the method. The two Label arguments are used to compute the start_pc and length items in each entry. The specification says (emphasis mine):

    The start_pc and length items indicate that the given local variable has a value at indices into the code array in the interval [start_pc, start_pc + length), that is, between start_pc inclusive and start_pc + length exclusive.

    Based on this, the placement of the labels in your code is quite odd. You are saying that hello "has a value" even before the astore instruction. Similarly, you are saying that goodbye "has a value" as soon as the else branch begins. This obviously doesn't make sense, and it is not surprising that a decompiler would behave unexpectedly.

    This is not about "marking" load/store instructions. This is about the range of code where that variable has a value. The first Label should be after the first instruction that assigns the variable a value, and the second Label should be before the variable "goes out of scope".

    Here I have added 4 labels to indicate the ranges for the 2 variables:

    var methodVisitor = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "something", "()V", null, null);
    methodVisitor.visitCode();
    Label label0 = new Label();
    methodVisitor.visitLabel(label0);
    methodVisitor.visitInsn(ICONST_1);
    Label label1 = new Label();
    methodVisitor.visitJumpInsn(IFEQ, label1);
    methodVisitor.visitLdcInsn("number");
    methodVisitor.visitVarInsn(ASTORE, 0);
    var helloStart = new Label();
    methodVisitor.visitLabel(helloStart);
    methodVisitor.visitVarInsn(ALOAD, 0);
    methodVisitor.visitMethodInsn(INVOKESTATIC, "org/bezsahara/minikotlin/NamingTests", "accept", "(Ljava/lang/Object;)V", false);
    var helloEnd = new Label();
    methodVisitor.visitLabel(helloEnd);
    methodVisitor.visitInsn(RETURN);
    methodVisitor.visitLabel(label1);
    methodVisitor.visitIntInsn(BIPUSH, 21);
    methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
    methodVisitor.visitVarInsn(ASTORE, 0);
    var goodbyeStart = new Label();
    methodVisitor.visitLabel(goodbyeStart);
    methodVisitor.visitVarInsn(ALOAD, 0);
    methodVisitor.visitMethodInsn(INVOKESTATIC, "org/bezsahara/minikotlin/NamingTests", "accept", "(Ljava/lang/Object;)V", false);
    var goodbyeEnd = new Label();
    methodVisitor.visitLabel(goodbyeEnd);
    methodVisitor.visitInsn(RETURN);
    Label label2 = new Label();
    methodVisitor.visitLabel(label2);
    methodVisitor.visitLocalVariable("hello", "Ljava/lang/String;", null, helloStart, helloEnd, 0);
    methodVisitor.visitLocalVariable("goodbye", "Ljava/lang/Integer;", null, goodbyeStart, goodbyeEnd, 0);
    methodVisitor.visitMaxs(0, 0);
    methodVisitor.visitEnd();
    

    The IntelliJ decompiler is now able to give the variables their expected names.