Search code examples
javabytecode

JVM Bytecode, how can I find the type of local variables?


I'm working on a fork of FernFlower from Jetbrains and I've been adding minor improvements to it.

One thing that really annoys me about FernFlower is that it bases the type of the local variable based on its value in bpush/spush etc. While Jode and Procyon somehow find a way to find the original value of a local variable.

Here is the original source code.

public static void main(String[] args) throws Exception {
    int hello = 100;
    char a2 = 100;
    short y1o = 100;
    int hei = 100;

    System.out.println(a2+" "+y1o+", "+hei+", "+hello);
}

When decompiled with FernFlower, it outputs this:

public static void main(String[] args) throws Exception {
    byte hello = 100;
    char a2 = 100;
    byte y1o = 100;
    byte hei = 100;
    System.out.println(a2 + " " + y1o + ", " + hei + ", " + hello);
}

But when decompiled with Jode/Procyon it outputs the original local variable types:

  public static void main(String[] args)
    throws Exception
  {
    int hello = 100;
    char a2 = 'd';
    short y1o = 100;
    byte hei = 100;

    System.out.println(a2 + " " + y1o + ", " + hei + ", " + hello);
  }

I was wondering how is this possible because I thought no local variable type information is stored at compile time? How can I add the same functionality to FernFlower?


Solution

  • So after looking around and debugging I found that for some reason FernFlower decides to completely ignore some of the data in the LocalVariableTable.

    Here is ferns original code for decoding the LocalVariableTable:

    public void initContent(ConstantPool pool) throws IOException {
        DataInputFullStream data = stream();
    
        int len = data.readUnsignedShort();
        if (len > 0) {
            mapVarNames = new HashMap<Integer, String>(len);
            for (int i = 0; i < len; i++) {
                data.discard(4);
                int nameIndex = data.readUnsignedShort();
                data.discard(2);
                int varIndex = data.readUnsignedShort();
                mapVarNames.put(varIndex, pool.getPrimitiveConstant(nameIndex).getString());
            }
        } else {
            mapVarNames = Collections.emptyMap();
        }
    }
    

    If you want type information you need to add the following:

    @Override
    public void initContent(ConstantPool pool) throws IOException {
        DataInputFullStream data = stream();
    
        int len = data.readUnsignedShort();
        if (len > 0) {
            mapVarNames = new HashMap<Integer, String>(len);
            mapVarTypes = new HashMap<Integer, String>(len);
            for (int i = 0; i < len; i++) {
                int start  = data.readUnsignedShort();
                int end    = start + data.readUnsignedShort();
                int nameIndex = data.readUnsignedShort();
                int typeIndex = data.readUnsignedShort();
                int varIndex = data.readUnsignedShort();
                mapVarNames.put(varIndex, pool.getPrimitiveConstant(nameIndex).getString());
                mapVarTypes.put(varIndex, pool.getPrimitiveConstant(typeIndex).getString());
            }
        } else {
            mapVarNames = Collections.emptyMap();
            mapVarTypes = Collections.emptyMap();
        }
    }
    

    It now outputs the same code as Jode with proper variable types :)

    I wonder why FernFlower chose to ignore this information.