Storyyeller / Krakatau

Java decompiler, assembler, and disassembler
GNU General Public License v3.0
1.95k stars 220 forks source link

StackMapTable attribute not generated automatically #83

Closed toddATavail closed 8 years ago

toddATavail commented 8 years ago

According to the documentation (Documentation/assembler.txt):

Note: The content of a StackMapTable attribute is automatically filled in based on the stack directives in the enclosing code attribute. If this attribute’s contents are nonempty and the attribute isn’t specified explicitly, one will be added implicitly. This means that you generally don’t have to specify it. It’s only useful if you care about the exact binary layout of the classfile.

However, I have not yet found a case where assemble.py actually produces the StackMapTable attribute. This includes cases where the JVM requires one.

Consider the following Krakatau source file, SumIntegersApp.j, which encodes a program that sums the command line arguments, treating them as stringified integers:

.version 52 0
.class public SumIntegersApp
        .super java/lang/Object
        .method public static main : ([Ljava/lang/String;)V
                .code stack 2 locals 3
                                iconst_0
                                dup
                                istore_1
                                istore_2
                        L1:
                                iload_2
                                aload_0
                                arraylength
                                if_icmpge L2
                                aload_0
                                iload_2
                                aaload
                                invokestatic java/lang/Integer parseInt (Ljava/lang/String;)I
                                iload_1
                                iadd
                                istore_1
                                iinc 2 1
                                goto L1
                        L2:
                                getstatic java/lang/System out Ljava/io/PrintStream;
                                iload_1
                                invokevirtual java/io/PrintStream println (I)V
                                return
                .end code
        .end method
.end class

The latest version of Krakatau from GitHub produces a version 52 Java class file from this source (assemble.py SumIntegersApp.j). Using javap -verbose -c produces:

Classfile SumIntegersApp.class
  Last modified Apr 7, 2016; size 346 bytes
  MD5 checksum e14a707303cf1469d680238245b582f2
public class SumIntegersApp
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC
Constant pool:
   #1 = Class              #25            // SumIntegersApp
   #2 = Class              #24            // java/lang/Object
   #3 = Utf8               main
   #4 = Utf8               ([Ljava/lang/String;)V
   #5 = Utf8               Code
   #6 = Methodref          #19.#20        // java/lang/Integer.parseInt:(Ljava/lang/String;)I
   #7 = Fieldref           #14.#15        // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Methodref          #9.#10         // java/io/PrintStream.println:(I)V
   #9 = Class              #13            // java/io/PrintStream
  #10 = NameAndType        #11:#12        // println:(I)V
  #11 = Utf8               println
  #12 = Utf8               (I)V
  #13 = Utf8               java/io/PrintStream
  #14 = Class              #18            // java/lang/System
  #15 = NameAndType        #16:#17        // out:Ljava/io/PrintStream;
  #16 = Utf8               out
  #17 = Utf8               Ljava/io/PrintStream;
  #18 = Utf8               java/lang/System
  #19 = Class              #23            // java/lang/Integer
  #20 = NameAndType        #21:#22        // parseInt:(Ljava/lang/String;)I
  #21 = Utf8               parseInt
  #22 = Utf8               (Ljava/lang/String;)I
  #23 = Utf8               java/lang/Integer
  #24 = Utf8               java/lang/Object
  #25 = Utf8               SumIntegersApp
{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: dup
         2: istore_1
         3: istore_2
         4: iload_2
         5: aload_0
         6: arraylength
         7: if_icmpge     25
        10: aload_0
        11: iload_2
        12: aaload
        13: invokestatic  #6                  // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
        16: iload_1
        17: iadd
        18: istore_1
        19: iinc          2, 1
        22: goto          4
        25: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_1
        29: invokevirtual #8                  // Method java/io/PrintStream.println:(I)V
        32: return
}

Note that the StackMapTable is missing for main. Being a version 52 class file, this is problematic, because attempting to run the class's main method produces:

Exception in thread "main" java.lang.VerifyError: Expecting a stackmap frame at branch target 25
Exception Details:
  Location:
    SumIntegersApp.main([Ljava/lang/String;)V @7: if_icmpge
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0x0000000: 0359 3c3d 1c2a bea2 0012 2a1c 32b8 0006
    0x0000010: 1b60 3c84 0201 a7ff eeb2 0007 1bb6 0008
    0x0000020: b1                                     

    at java.lang.Class.getDeclaredMethods0(Native Method)
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
    at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
    at java.lang.Class.getMethod0(Class.java:3018)
    at java.lang.Class.getMethod(Class.java:1784)
    at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)

Setting the version to 50 produces a working class file, and the program executes as expected, but only because class files with version < 51 do not require StackMapTable attributes.

I am using Krakatau as the back end of an optimizing compiler for a domain specific language, and I chose this technology in large part because of the quoted passage at the beginning of my post; I did not want to have to compute my own StackMapTables. This issue is critical for me; I might be able to get away with using version 50 class files in the short term, but not forever.

Is it possible that I am not invoking Krakatau correctly? Do you see any problems with my source file, my procedure, or my reasoning? Thanks in advance for your help.

Storyyeller commented 8 years ago

Sorry, I guess the wording is confusing.

If you have any .stack directives in a method, Krakatau generates a StackMapTable attribute, filled in with the data from those directives. What it was means is that you don't also have to say ".stackmaptable". If you don't have any .stack directives, it won't implicitly generate a StackMapTable attribute.

If you are generating bytecode yourself, I would highly recommend using version 49.0 (or 50.0, I guess). Creating stack maps is a huge pain. And old classfiles versions are completely interoperable with new ones. The only downside is that you can't use invokedynamic or other new features.

samczsun commented 8 years ago

You might be able to generate the class file using Krakatau, then run it through ASM using COMPUTE_FRAMES to create the frames. That's assuming that you are able to integrate Java into your optimizing compiler

Storyyeller commented 8 years ago

As far writing a tool to generate stack maps for you from bytecode, the biggest issue I can think of is how to handle type merging. Without knowing the inheritance hierarchies of the classes in advance, you can't determine the lowest common superclass or which classes are interfaces.

Apart from that, it would require work and it's not a priority for me.

Also, I second the suggestion to try ASM.

toddATavail commented 8 years ago

Thanks for the advice. I'll look into using ASM to rewrite the class files with the computed stack maps.

You might want to reword the passage that confused me, lest it confuse anyone else. Krakatau is a pretty good tool. It would certainly be nice if it could generate stack map tables also, as this would reverse a lot of the harm introduced by Oracle in necessitating computation of the stack map tables by users. A clean JVM assembler that is able to automatically generate correct stack map tables would be a tremendous asset to the community; incidentally, it's what I thought that Krakatau already was, and what drove me to use it.

Thanks again.

toddATavail commented 8 years ago

ASM worked for me, by the way. I am now using ASM to add stack map tables to classes generated by Krakatau. I have complete control over the compiler, so it will be easy to integrate this extra step into the code generation process. Thanks for the good advice!

toddATavail commented 8 years ago

Storyyeller: Out of curiosity, would you like the program that I wrote to leverage ASM to add stack map tables to existing class files? It's a small, clean, well-documented, one-file Java application that uses only ASM and the JRE. It doesn't exactly fit in with your Python code, but it's a good complement to Krakatau itself, and might make your product even more attractive to developers with similar use cases. Presumably users of Krakatau already have access to Java technology; they might not have ASM already, but it's a good bet that they can get it if needed.

My company, My Coverage Plan, Inc., has authorized me to release this program for you to redistribute, so long as 1) it remains under an open-source license that acknowledges our contribution and 2) we can continue to use it under either the new or original license.

If you're not interested, then perhaps you wouldn't mind if I approached the authors of ASM with the same offer? They didn't seem to have anything like this already written up, and I have to imagine that it's a fairly common use case ever since Oracle tightened the thumbscrews on 1.7. But I'd like to offer it to you first, since it's a useful augment to Krakatau.

Storyyeller commented 8 years ago

I think it would be a better fit with ASM. If they don't want it, I'd suggest uploading it to Github seperately.