Search code examples
javajvminstrumentationjava-bytecode-asmjvm-bytecode

When instrumenting classes, offset of inserted stack frame overlap with existing one


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?


Solution

  • 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.