我无法在 ASM JAVA 中为 invokedynamic 加载局部变量

I can't load local variables for invokedynamic in ASM JAVA

我已经为方法创建了一个迷你记录器,并且我使用了 ASM。我需要通过描述符方法参数来确定并打印它。 但是我有一个错误

Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    ru/otus/TestLogging.calc(IFD)V @6: invokedynamic
  Reason:
    Type 'java/io/PrintStream' (current frame, stack[4]) is not assignable to double_2nd
  Current Frame:
    bci: @6
    flags: { }
    locals: { 'ru/otus/TestLogging', integer, float, double, double_2nd }
    stack: { 'ru/otus/TestLogging', float, double, double_2nd, 'java/io/PrintStream' }
  Bytecode:
    0000000: 2a24 29b2 0007 ba00 3e00 00b6 0011 b200
    0000010: 071b 2429 ba00 0d00 00b6 0011 b1      

这是我的代理人的代码

public class Agent {

  public static void premain(String agentArgs, Instrumentation inst) {

    inst.addTransformer(new ClassFileTransformer() {
        @Override
        public byte[] transform(ClassLoader loader,
                                String className,
                                Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain,
                                byte[] classfileBuffer) throws IllegalClassFormatException {
                if(className.contains("ru/otus/")) {
                    return changeMethod(classfileBuffer, className);
                }
                return classfileBuffer;
        }
    });
  }

  private static byte[] changeMethod(byte[] originalClass, String className) {
      ClassReader reader = new ClassReader(originalClass);
      ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
      ArrayList<String> list = new ArrayList<>();
      ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5, writer) {

          @Override
          public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions){
              System.out.println("visitMethod: access="+access+" name="+name+" desc="+descriptor+" signature="+signature+" exceptions="+exceptions);
              Method thisMethod = new Method(name, descriptor);

              MethodVisitor mv = new MethodAnnotationScanner(Opcodes.ASM5, super.visitMethod(access, name, descriptor, signature, exceptions), thisMethod, className);
              return mv;
          }
      };

      reader.accept(visitor, Opcodes.ASM5);

      for(String methodName : list) {
          System.out.println(methodName);
      }

      byte[] finalClass = writer.toByteArray();

      if(className.contains("Test")) {
          try (OutputStream fos = new FileOutputStream("TestLogging.class")) {
              fos.write(finalClass);
          } catch (Exception e) {
              e.printStackTrace();
          }
      }

      return writer.toByteArray();
  }

  static class MethodAnnotationScanner extends MethodVisitor {

      private Method thisMethod;
      private boolean isChangeMethod = false;
      private String className = null;
      private StringBuilder descriptor = new StringBuilder("(");

      public MethodAnnotationScanner(int api, MethodVisitor methodVisitor, Method thisMethod, String className) {
          super(api, methodVisitor);
          this.thisMethod = thisMethod;
          this.className = className;
      }

      @Override
      public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
          System.out.println("visitAnnotation: desc="+desc+" visible="+visible);
          if(desc.contains("ru/otus/annotations/Log")) {
              this.isChangeMethod = true;
              return super.visitAnnotation(desc, visible);
          }
          this.isChangeMethod = false;
          return super.visitAnnotation(desc, visible);
      }

      @Override
      public void visitCode() {
          if(this.isChangeMethod) {
              super.visitVarInsn(Opcodes.ALOAD, 0);
                  int i = 1;
                  for(Type arg : thisMethod.getArgumentTypes()) {
                      this.descriptor.append(arg.getDescriptor());
                      if (arg.getDescriptor().equals("J")) {
                          super.visitVarInsn(Opcodes.LLOAD, i);
                          ++i;
                      } else if (arg.getDescriptor().equals("D")) {
                          super.visitVarInsn(Opcodes.DLOAD, i);
                          ++i;
                      } else if (arg.getDescriptor().equals("F")) {
                          super.visitVarInsn(Opcodes.FLOAD, i);
                      } else if(arg.getDescriptor().equals("I")) {
                          super.visitVarInsn(Opcodes.ILOAD, i);
                      }
                      i++;
                  }


                  Handle handle = new Handle(
                          H_INVOKESTATIC,
                          Type.getInternalName(java.lang.invoke.StringConcatFactory.class),
                          "makeConcatWithConstants",
                          MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, Object[].class).toMethodDescriptorString(),
                          false);
                  this.descriptor.append(")Ljava/lang/String;");
                  super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                  super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName()  + ", param: \u0001".repeat(i));
                  super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                  super.visitMaxs(0, 0);
              }

          if (mv != null) {
              super.visitCode();
          }
          super.visitEnd();
      }
   }
 }

我有两个 class 可以重现此功能。 首先 - 测试记录 第二个 - AutoLogger

在第一个class中,我有一个方法需要记录, 第二个开始 class 包含方法 main.

This is my project

您在读取字段 System.out 之前推送字符串连接的参数,然后尝试执行字符串连接。因此,用于执行字符串连接的 invokedynamic 指令在操作数堆栈上发现了不匹配的 PrintStream

一个简单的解决方法是更改​​说明

super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName()  + ", param: \u0001".repeat(i));
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName()  + ", param: \u0001".repeat(i));
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitInsn(Opcodes.SWAP);
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

或者将 GETSTATIC 移动到将推送 concat 参数的代码之前,到执行过时的 super.visitVarInsn(Opcodes.ALOAD, 0); 的位置。那么,你不需要SWAP.

但是代码的问题比较多。您在推送值时计算局部变量,正确考虑 longdouble 取两个变量,但是,您在 ", param: \u0001".repeat(i) 表达式中使用相同的数字,这将告诉StringConcatFactorylongdouble 的情况下有两个值。你需要分开柜台。此外,您没有推送引用类型参数,但由于您正在对它们进行计数并将它们包含在 concat 调用的签名中,因此您还必须将它们推送到操作数堆栈。

此外,虽然在这里没有效果,但 visitMaxs(0, 0) 调用和 visitEnd() 调用是不合适的。您在代码的开头注入,您不会拦截的其他访问调用将跟随,包括自动执行的 visitMaxsvisitEnd.

所有修复后,代码看起来像

public void visitCode() {
    if(this.isChangeMethod) {
        super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        int varIndex = 1, numArgs = 0;
        for(Type arg : thisMethod.getArgumentTypes()) {
            this.descriptor.append(arg.getDescriptor());
            if (arg.getDescriptor().equals("J")) {
                super.visitVarInsn(Opcodes.LLOAD, varIndex);
                ++varIndex;
            } else if (arg.getDescriptor().equals("D")) {
                super.visitVarInsn(Opcodes.DLOAD, varIndex);
                ++varIndex;
            } else if (arg.getDescriptor().equals("F")) {
                super.visitVarInsn(Opcodes.FLOAD, varIndex);
            } else if(arg.getDescriptor().equals("I")) {
                super.visitVarInsn(Opcodes.ILOAD, varIndex);
            } else {
                super.visitVarInsn(Opcodes.ALOAD, varIndex);
            }
            varIndex++;
            numArgs++;
        }

        Handle handle = new Handle(
            H_INVOKESTATIC,
            Type.getInternalName(java.lang.invoke.StringConcatFactory.class),
            "makeConcatWithConstants",
            MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, Object[].class).toMethodDescriptorString(),
            false);
        this.descriptor.append(")Ljava/lang/String;");
        super.visitInvokeDynamicInsn("makeConcatWithConstants", this.descriptor.toString(), handle, "executed method: " + this.thisMethod.getName()  + ", param: \u0001".repeat(numArgs));
        super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }
    super.visitCode();
}

附带说明一下,当你为连接推送所有参数时,你可以简化描述符的构造,因为连接描述符几乎与方法的描述符相同;您只需将 return 类型替换为 Ljava/lang/String;.

您可以在不处理 MethodType 对象的情况下完成整个操作,只需使用已经传递给 visitMethod.

的两个字符串
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
                                 String signature, String[] exceptions) {
    System.out.println("visitMethod: access="+access+" name="+name
        +" desc="+descriptor+" signature="+signature+" exceptions="+exceptions);
    MethodVisitor mv = new MethodAnnotationScanner(Opcodes.ASM5, name, descriptor,
        super.visitMethod(access, name, descriptor, signature, exceptions));
    return mv;
}
static class MethodAnnotationScanner extends MethodVisitor {

    private boolean isChangeMethod;
    private final String name, descriptor;

    public MethodAnnotationScanner(int api, String name,
                                   String methodDesciptor, MethodVisitor methodVisitor){
        super(api, methodVisitor);
        this.name = name;
        this.descriptor = methodDesciptor;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        System.out.println("visitAnnotation: desc="+desc+" visible="+visible);
        if(desc.contains("ru/otus/annotations/Log")) {
            this.isChangeMethod = true;
            return super.visitAnnotation(desc, visible);
        }
        this.isChangeMethod = false;
        return super.visitAnnotation(desc, visible);
    }

    @Override
    public void visitCode() {
        if(this.isChangeMethod) {
            super.visitFieldInsn(Opcodes.GETSTATIC,
                                 "java/lang/System", "out", "Ljava/io/PrintStream;");
            int varIndex = 1, numArgs = 0, p;
            for(p = 1; descriptor.charAt(p) != ')'; p++) {
                switch(descriptor.charAt(p)) {
                    case 'J':
                        super.visitVarInsn(Opcodes.LLOAD, varIndex); ++varIndex; break;
                    case 'D':
                        super.visitVarInsn(Opcodes.DLOAD, varIndex); ++varIndex; break;
                    case 'F': super.visitVarInsn(Opcodes.FLOAD, varIndex); break;
                    case 'I': super.visitVarInsn(Opcodes.ILOAD, varIndex); break;
                    case 'L': super.visitVarInsn(Opcodes.ALOAD, varIndex);
                        p = descriptor.indexOf(';', p);
                        break;
                    case '[': super.visitVarInsn(Opcodes.ALOAD, varIndex);
                        do {} while(descriptor.charAt(++p)=='[');
                        if(descriptor.charAt(p) == 'L') p = descriptor.indexOf(';', p);
                        break;
                    default: throw new IllegalStateException(descriptor);
                }
                varIndex++;
                numArgs++;
            }
            String ret = "Ljava/lang/String;";
            String concatSig = new StringBuilder(++p + ret.length())
                .append(descriptor, 0, p).append(ret).toString();

            Handle handle = new Handle(
                H_INVOKESTATIC,
                "java/lang/invoke/StringConcatFactory",
                "makeConcatWithConstants",
                MethodType.methodType(CallSite.class, MethodHandles.Lookup.class,
                    String.class, MethodType.class, String.class, Object[].class)
                    .toMethodDescriptorString(),
                false);
            super.visitInvokeDynamicInsn("makeConcatWithConstants", concatSig, handle,
                "executed method: " + name  + ", param: \u0001".repeat(numArgs));
            super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        super.visitCode();
    }
}