动态字节码检测 - 问题

发布于 2024-07-29 19:47:47 字数 1419 浏览 2 评论 0原文

我有一个无法解决的问题。 假设我们有以下两个类和继承关系:

public class A {
}

public class B extends A {
    public void foo() {}
}

我想检测附加代码,使其如下所示:

public class A {
    public void print() { }
}

public class B extends A {
     public void foo() { print(); }
}

为了实现此目标,我基于 java.lang.instrument 包,使用带有我自己的类文件转换器的代理。 该机制也称为动态字节码检测。

到目前为止小菜一碟。 现在,我的测试方法执行以下操作:

代码:

B b = new B();
b.foo();

由于检测包中存在以下限制,这不起作用:当调用 new B() 时,检测以类 B 开始并以 a 结束加载被操作的类时出现编译错误,因为超类 A 还没有 print() 方法! 问题是我是否以及如何在类 B 之前触发类 A 的检测。我的 classfiletransformer 的 Transform() 方法应该与类 A 一起显式调用! 所以我开始阅读并遇到了这个:

java.lang.instrument.ClassFileTransformer.transform()'s javadoc 说:

变压器将被要求 每个新的类定义和每个 类的重新定义。 请求 新的类定义是用 类加载器.defineClass。 请求 对于类重新定义是用 Instrumentation.redefineClasses 或其 本机等效项。

Transform 方法附带了一个类加载器实例,所以我想,为什么不自己用类调用 loadClass 方法(loadClass 调用 defineClass) A 当 B 的检测开始时。 我预计仪器方法将被调用,但遗憾的是事实并非如此。 相反,类 A 是在没有检测的情况下加载的。 (代理不会拦截加载过程,尽管它应该这样做)

有什么想法,如何解决这个问题? 您是否明白为什么操作某些字节码的代理不可能手动加载另一个类(然后希望也通过该/任何代理发送)的原因?

请注意,以下代码可以正常工作,因为 A 在操作 B 之前已加载并检测。

A a =  new A();
B b = new B();
b.foo();

多谢!

I have a problem I am not able to solve. Let's assume we have the following two classes and an inheritance relationship:

public class A {
}

public class B extends A {
    public void foo() {}
}

I want to instrument additional code such that it looks as follows:

public class A {
    public void print() { }
}

public class B extends A {
     public void foo() { print(); }
}

In order to achieve this goal, I based my implementation on the java.lang.instrument package, using an Agent with my own class file transformer. The mechanism is also referred to as dynamic bytecode instrumentation.

Piece of cake so far.
Now, my test method does the following:

Code:

B b = new B();
b.foo();

This does not work due to the following restriction in the instrumentation package: when calling new B(), the instrumentation starts with class B and ends up in a compilation error when loading the manipulated class as the super class A has no print() method yet! The question arises if and how I can trigger the instrumentation of class A before class B. The transform() method of my classfiletransformer should be invoked with class A explicitly! So I started reading and bumped into this:

The java.lang.instrument.ClassFileTransformer.transform()'s javadoc says:

The transformer will be called for
every new class definition and every
class redefinition. The request for a
new class definition is made with
ClassLoader.defineClass. The request
for a class redefinition is made with
Instrumentation.redefineClasses or its
native equivalents.

The transform method comes along with a class loader instance, so I thought, why not calling the loadClass method (loadClass calls defineClass) myself with class A when the instrumentation of B has started.
I expected that the instrument method will be called as a result but sadly this was not the case. Instead the class A was loaded without instrumentation. (The agent does not intercept the load process although it is supposed to)

Any ideas, how to solve this problem? Do you see a reason why it is not possible that an agent that manipulates some bytecode cannot manually load another class that is then hopefully also send through that/any agent?

Note that the following code works properly since A was loaded and instrumented before B is manipulated.

A a =  new A();
B b = new B();
b.foo();

Thanks a lot!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

野侃 2024-08-05 19:47:47

当我在 Sun 1.6.0_15 和 1.5.0_17 JRE 上先转换 B 时,我没有看到任何问题(我使用 ASM< /a>)。 我会通过在外部运行转换代码并检查生成的类(例如使用 javap)来仔细检查转换代码。 我还会检查您的类路径配置,以确保由于某种原因 A 不会在您的代理之前加载(也许使用 getAllLoadedClasses)。


编辑:

如果您在代理中加载类 A ,如下所示:

Class.forName("A");

...然后抛出异常:

Exception in thread "main" java.lang.NoSuchMethodError: B.print()V

这是有道理的 - A 成为代理的依赖项,并且它会对于代理来说,检测自己的代码是没有意义的。 你会得到一个无限循环,导致堆栈溢出。 因此,A 不会被ClassFileTransformer 处理。


为了完整起见,这是我的测试代码,可以正常工作。 如前所述,它取决于 ASM 库。

代理:

public class ClassModifierAgent implements ClassFileTransformer {

  public byte[] transform(ClassLoader loader, String className,
      Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
      byte[] classfileBuffer) throws IllegalClassFormatException {
    System.out.println("transform: " + className);
    if ("A".equals(className)) {
      return new AModifier().modify(classfileBuffer);
    }
    if ("B".equals(className)) {
      return new BModifier().modify(classfileBuffer);
    }
    return classfileBuffer;
  }

  /** Agent "main" equivalent */
  public static void premain(String agentArguments,
      Instrumentation instrumentation) {
    instrumentation.addTransformer(new ClassModifierAgent());
  }

}

A的方法注入器:

public class AModifier extends Modifier {

  @Override
  protected ClassVisitor createVisitor(ClassVisitor cv) {
    return new AVisitor(cv);
  }

  private static class AVisitor extends ClassAdapter {

    public AVisitor(ClassVisitor cv) { super(cv); }

    @Override
    public void visitEnd() {
      MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "print", "()V",
          null, null);
      mv.visitCode();
      mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
          "Ljava/io/PrintStream;");
      mv.visitLdcInsn("X");
      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
          "println", "(Ljava/lang/String;)V");
      mv.visitInsn(Opcodes.RETURN);
      mv.visitMaxs(2, 1);
      mv.visitEnd();

      super.visitEnd();
    }

  }

}

B的方法替换器:

public class BModifier extends Modifier {

  @Override
  protected ClassVisitor createVisitor(ClassVisitor cv) {
    return new BVisitor(cv);
  }

  class BVisitor extends ClassAdapter {

    public BVisitor(ClassVisitor cv) { super(cv); }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions) {
      if ("foo".equals(name)) {
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "foo", "()V",
            null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "B", "print", "()V");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
        return new EmptyVisitor();
      } else {
        return super.visitMethod(access, name, desc, signature, exceptions);
      }
    }
  }
}

通用基础代码:

public abstract class Modifier {

  protected abstract ClassVisitor createVisitor(ClassVisitor cv);

  public byte[] modify(byte[] data) {
    ClassReader reader = new ClassReader(data);
    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
    ClassVisitor visitor = writer;
    visitor = new CheckClassAdapter(visitor);
    visitor = createVisitor(visitor);
    reader.accept(visitor, 0);
    return writer.toByteArray();
  }

}

对于一些可见的结果,我添加了System.out.println('X ');A.print()

当运行此代码时:

public class MainInstrumented {
  public static void main(String[] args) {
    new B().foo();
  }
}

...它会产生以下输出:

transform: MainInstrumented
transform: B
transform: A
X

I did not see any issues when I transformed B before A on the Sun 1.6.0_15 and 1.5.0_17 JREs (I used ASM). I would double-check the transformation code by running it externally and inspecting the resultant classes (e.g. with javap). I'd also check your classpath configuration to ensure A isn't loaded before your agent for some reason (perhaps check in your premain with getAllLoadedClasses).


EDIT:

If you load class A in your agent like this:

Class.forName("A");

...then an exception is thrown:

Exception in thread "main" java.lang.NoSuchMethodError: B.print()V

This makes sense - A becomes a dependency of the agent and it would not make sense for the agent to instrument its own code. You'd get an infinite loop that resulted in a stack overflow. Therefore, A is not processed by the ClassFileTransformer.


For completeness, here is my test code that works without problem. As mentioned, it depends on the ASM library.

The agent:

public class ClassModifierAgent implements ClassFileTransformer {

  public byte[] transform(ClassLoader loader, String className,
      Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
      byte[] classfileBuffer) throws IllegalClassFormatException {
    System.out.println("transform: " + className);
    if ("A".equals(className)) {
      return new AModifier().modify(classfileBuffer);
    }
    if ("B".equals(className)) {
      return new BModifier().modify(classfileBuffer);
    }
    return classfileBuffer;
  }

  /** Agent "main" equivalent */
  public static void premain(String agentArguments,
      Instrumentation instrumentation) {
    instrumentation.addTransformer(new ClassModifierAgent());
  }

}

Method injector for A:

public class AModifier extends Modifier {

  @Override
  protected ClassVisitor createVisitor(ClassVisitor cv) {
    return new AVisitor(cv);
  }

  private static class AVisitor extends ClassAdapter {

    public AVisitor(ClassVisitor cv) { super(cv); }

    @Override
    public void visitEnd() {
      MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "print", "()V",
          null, null);
      mv.visitCode();
      mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
          "Ljava/io/PrintStream;");
      mv.visitLdcInsn("X");
      mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
          "println", "(Ljava/lang/String;)V");
      mv.visitInsn(Opcodes.RETURN);
      mv.visitMaxs(2, 1);
      mv.visitEnd();

      super.visitEnd();
    }

  }

}

Method replacer for B:

public class BModifier extends Modifier {

  @Override
  protected ClassVisitor createVisitor(ClassVisitor cv) {
    return new BVisitor(cv);
  }

  class BVisitor extends ClassAdapter {

    public BVisitor(ClassVisitor cv) { super(cv); }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions) {
      if ("foo".equals(name)) {
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "foo", "()V",
            null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "B", "print", "()V");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
        return new EmptyVisitor();
      } else {
        return super.visitMethod(access, name, desc, signature, exceptions);
      }
    }
  }
}

Common base code:

public abstract class Modifier {

  protected abstract ClassVisitor createVisitor(ClassVisitor cv);

  public byte[] modify(byte[] data) {
    ClassReader reader = new ClassReader(data);
    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
    ClassVisitor visitor = writer;
    visitor = new CheckClassAdapter(visitor);
    visitor = createVisitor(visitor);
    reader.accept(visitor, 0);
    return writer.toByteArray();
  }

}

For some visible results, I added a System.out.println('X'); to A.print().

When run on this code:

public class MainInstrumented {
  public static void main(String[] args) {
    new B().foo();
  }
}

...it produces this output:

transform: MainInstrumented
transform: B
transform: A
X
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文