2

What I did is to instrument java classes at runtime to warp the whole method with a big try-catch block, and then rethrow the exception in the catch block if any exception is caught.

I use a Premain class to add a class file transformer to dynamically instrument the loaded java class. The Transformer class:

public class Transformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws
            IllegalClassFormatException {
        byte[] result = classfileBuffer;
        if (className == null){
            return result;
        }
        ...
        try{
            ClassReader cr = new ClassReader(result);
            ClassWriter cw = new ClassWriter(cr, 0);
            ClassVisitor cv = new CV(cw, className, loader, getClassVersion(cr));
            cr.accept(cv, ClassReader.EXPAND_FRAMES);
            result = cw.toByteArray();
        } catch (Throwable t){
            t.printStackTrace();
        }
        return result;
    }
}

The ClassVisitor and MethodVisitor:

public class CV extends ClassVisitor {
    private String slashClassName;
    private ClassLoader loader;
    private int classVersion;

    public CV(ClassVisitor classVisitor, String className, ClassLoader loader, int classVersion) {
        super(ASM_Version, classVisitor);
        this.slashClassName = className;
        this.loader = loader;
        this.classVersion = classVersion;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new MV(mv, slashClassName, name, descriptor, this.classVersion);
    }
}

class MV extends MethodVisitor {
    ...
    // insert a try catch block for the whole test method to capture the exception thrown
    private Label tryStart = new Label();
    private Label tryEndCatchStart = new Label();

    public MV(MethodVisitor methodVisitor, String className, String methodName, String desc, int classVersion) {
        super(ASM_Version, methodVisitor);
        ...
    }
    @Override
    public void visitCode() {
        mv.visitCode();
        mv.visitLabel(tryStart);
    }
    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        mv.visitTryCatchBlock(tryStart, tryEndCatchStart, tryEndCatchStart, "java/lang/Throwable");
        mv.visitLabel(tryEndCatchStart);
        mv.visitFrame(F_FULL, 0, null, 1, new Object[]{"java/lang/Throwable"});
        mv.visitInsn(ATHROW);
        mv.visitMaxs(maxStack+4, maxLocals);
    }
}

But I got an error like this:

...
Caused by: java.lang.VerifyError: StackMapTable error: bad offset
Exception Details:
  Location:
    org/example/Test.personTest()V @0: new
  Reason:
    Invalid stackmap specification.
  Current Frame:
    bci: @147
    flags: { }
    locals: { }
    stack: { 'java/lang/Throwable' }
  Bytecode:
    0x0000000: bb00 0259 bb00 0359 1204 1205 b700 06b7
    0x0000010: 0007 b300 08b2 0008 bb00 0359 1209 120a
    0x0000020: b700 06b5 000b b200 0cb2 000d b600 0eb2
    0x0000030: 0008 b400 0b12 0fb5 0010 2ab7 0011 9900
    0x0000040: 0bb2 000c 1212 b600 13b1 bf            
  Exception Handler Table:
    bci [0, 74] => handler: 74
  Stackmap Table:
    same_frame_extended(@73)
    full_frame(@147,{},{Object[#84]})

The original class before instrumented looks like:

  public void personTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=6, locals=1, args_size=1
         0: new           #2                  // class org/example/Person
         3: dup
         4: new           #3                  // class org/example/Name
         7: dup
         8: ldc           #4                  // String xxx
        10: ldc           #5                  // String yyy
        12: invokespecial #6                  // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
        15: invokespecial #7                  // Method org/example/Person."<init>":(Lorg/example/Name;)V
        18: putstatic     #8                  // Field president:Lorg/example/Person;
        21: getstatic     #8                  // Field president:Lorg/example/Person;
        24: new           #3                  // class org/example/Name
        27: dup
        28: ldc           #9                  // String a
        30: ldc           #10                 // String b
        32: invokespecial #6                  // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
        35: putfield      #11                 // Field org/example/Person.name:Lorg/example/Name;
        38: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        41: getstatic     #13                 // Field name:Lorg/example/Name;
        44: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        47: getstatic     #8                  // Field president:Lorg/example/Person;
        50: getfield      #11                 // Field org/example/Person.name:Lorg/example/Name;
        53: ldc           #15                 // String c
        55: putfield      #16                 // Field org/example/Name.familyName:Ljava/lang/String;
        58: aload_0
        59: invokespecial #17                 // Method ifxxx:()Z
        62: ifeq          73
        65: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        68: ldc           #18                 // String yes
        70: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        73: return
      LineNumberTable:
        line 33: 0
        line 35: 21
        line 36: 38
        line 37: 47
        line 38: 58
        line 39: 65
        line 41: 73
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      74     0  this   Lorg/example/Test;
      StackMapTable: number_of_entries = 1
        frame_type = 251 /* same_frame_extended */
          offset_delta = 73
    RuntimeVisibleAnnotations:
      0: #40()

The class after instrumentation looks like:

  public void personTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=10, locals=1, args_size=1
         0: new           #2                  // class org/example/Person
         3: dup
         4: new           #3                  // class org/example/Name
         7: dup
         8: ldc           #4                  // String xxx
        10: ldc           #5                  // String yyy
        12: invokespecial #6                  // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
        15: invokespecial #7                  // Method org/example/Person."<init>":(Lorg/example/Name;)V
        18: putstatic     #8                  // Field president:Lorg/example/Person;
        21: getstatic     #8                  // Field president:Lorg/example/Person;
        24: new           #3                  // class org/example/Name
        27: dup
        28: ldc           #9                  // String a
        30: ldc           #10                 // String b
        32: invokespecial #6                  // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
        35: putfield      #11                 // Field org/example/Person.name:Lorg/example/Name;
        38: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        41: getstatic     #13                 // Field name:Lorg/example/Name;
        44: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        47: getstatic     #8                  // Field president:Lorg/example/Person;
        50: getfield      #11                 // Field org/example/Person.name:Lorg/example/Name;
        53: ldc           #15                 // String c
        55: putfield      #16                 // Field org/example/Name.familyName:Ljava/lang/String;
        58: aload_0
        59: invokespecial #17                 // Method ifxxx:()Z
        62: ifeq          73
        65: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        68: ldc           #18                 // String yes
        70: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        73: return
        74: athrow
      Exception table:
         from    to  target type
             0    74    74   Class java/lang/Throwable
      StackMapTable: number_of_entries = 2
        frame_type = 251 /* same_frame_extended */
          offset_delta = 73
        frame_type = 255 /* full_frame */
          offset_delta = 73
          locals = []
          stack = [ class java/lang/Throwable ]
      LineNumberTable:
        line 33: 0
        line 35: 21
        line 36: 38
        line 37: 47
        line 38: 58
        line 39: 65
        line 41: 73
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      74     0  this   Lorg/example/Test;
    RuntimeVisibleAnnotations:
      0: #40()

I found that the two frames in the StackMapTable have the same offset 73! That is absolutely wrong because

The bytecode offset at which a frame applies is calculated by adding offset_delta + 1 to the bytecode offset of the previous frame, unless the previous frame is the initial frame of the method, ...

So that is why it complains. However, I still don't know why the inserted frame has the same offset as the existing one (shouldn't it be 0?), is that just a coincidence?

Instein
  • 2,484
  • 1
  • 9
  • 14
  • 1
    I suppose you’re not using the `COMPUTE_FRAMES` option but trying to retain the original frames. So it’s worth disassembling the original code as it is before the instrumentation and include it. As a stylistic note, you should use `super.visit…` instead of `mv.visit…` – Holger Oct 14 '22 at 06:49
  • @Holger Yes I am not using `COMPUTE_FRAMES`. Thanks for pointing out the `super.visit..` issue I didn't notice the problem. Now I add the code of Transformer, ClassVisitor, and the class before instrumentation. – Instein Oct 14 '22 at 14:21
  • 1
    I don’t know whether that’s the issue, but you’re combining `EXPAND_FRAMES` with `F_FULL`. According to the documentation, all frame types must be `F_NEW` when using expanded frames. Since you’re not manipulating frames further (do not require expanded frames), the more efficient solution is to remove the `EXPAND_FRAMES` option and stay with `F_FULL`. – Holger Oct 14 '22 at 14:53
  • Then error changes t```Exception Details: Location: org/example/Test.()V @5: athrow Reason: Current frame's flags are not assignable to stack map frame's. Current Frame: bci: @0 flags: { flagThisUninit } locals: { uninitializedThis } stack: { 'java/lang/Throwable' } Stackmap Frame: bci: @5 flags: { } locals: { } stack: { 'java/lang/Throwable' } Bytecode: 0x0000000: 2ab7 0001 b1bf Exception Handler Table: bci [0, 5] => handler: 5 Stackmap Table: full_frame(@5,{},{Object[#84]})``` – Instein Oct 14 '22 at 15:22
  • 1
    This means, you’re not failing at the `personTest()` method anymore, but at a constructor. This has [recently been discussed](https://stackoverflow.com/q/73962302/2711488); you can not wrap the entire constructor (which includes the `super(…)` call) with an exception handler. This is a different issue. Oh, I just noticed, [the original question](https://stackoverflow.com/q/69549215/2711488) was yours… – Holger Oct 14 '22 at 15:35
  • Yeah I just remember . Now the errors are gone. Thanks a lot!! By the way, where did you find the documentation about `EXPAND_FRAMES`? I can only find a description of this flag in https://asm.ow2.io/javadoc/org/objectweb/asm/ClassReader.html#EXPAND_FRAMES but can't really understand what it means. Do you have any recommendations about related reading materials? – Instein Oct 14 '22 at 15:47

1 Answers1

2

You are mixing two incompatible options, EXPAND_FRAMES and F_FULL. From the documentation of visitFrame:

The frames of a method must be given either in expanded form, or in compressed form (all frames must use the same format, i.e. you must not mix expanded and compressed frames within a single method):

  • In expanded form, all frames must have the F_NEW type.
  • In compressed form, frames are basically "deltas" from the state of the previous frame:
    • Opcodes.F_SAME representing frame with exactly the same locals as the previous frame and with the empty stack.
    • Opcodes.F_SAME1 representing frame with exactly the same locals as the previous frame and with single value on the stack (numStack is 1 and stack[0] contains value for the type of the stack item).
    • Opcodes.F_APPEND representing frame with current locals are the same as the locals in the previous frame, except that additional locals are defined (numLocal is 1, 2 or 3 and local elements contains values representing added types).
    • Opcodes.F_CHOP representing frame with current locals are the same as the locals in the previous frame, except that the last 1-3 locals are absent and with the empty stack (numLocal is 1, 2 or 3).
    • Opcodes.F_FULL representing complete frame data.

Since you’re keeping all original frames and adding just one frame, it’s more efficient to remove the EXPAND_FRAMES option, to keep all frames compressed.

Holger
  • 285,553
  • 42
  • 434
  • 765