java.lang.instrument 中的堆栈溢出错误与 ASM 字节码转换

Stack overflow error in java.lang.instrument with ASM bytecode transformation

我是 Java 代理检测和 ASM 字节码检测的新手。我从 this UCLA tutorial and used it for javagent instrumentation using java.lang.instrument.

中获取了代码

第一个问题,ASM 字节码库中是否有任何与 javaagent 检测不兼容的内容?

这里是经过编辑的程序:

public class Instrumenter {
    public static void premain(String args, Instrumentation inst) throws Exception {
        Transformer tr = new Transformer();
        inst.addTransformer(tr);
    }

}

class Transformer implements ClassFileTransformer {

    public Transformer() {
    }

    @Override
    public byte[] transform( ClassLoader loader, String className, Class<?> klass, ProtectionDomain domain, byte[] klassFileBuffer ) throws IllegalClassFormatException {
        byte[] barray;
        ClassWriter cwriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassReader creader;
        try {
            creader = new ClassReader(new ByteArrayInputStream(klassFileBuffer));
        } catch (Exception exc) {
            throw new IllegalClassFormatException(exc.getMessage());
        }
        ClassVisitor cvisitor = new ClassAdapter(cwriter);
        creader.accept(cvisitor, 0);
        barray = cwriter.toByteArray();
        return barray;
    }

}

class ClassAdapter extends ClassVisitor implements Opcodes {
    public ClassAdapter(ClassVisitor cv) {
        super(ASM7, cv);
    }


    @Override
    public MethodVisitor visitMethod( final int access, final String name, final String desc, final String signature, final String[] exceptions ) {
        this.pwriter.println(ClassAdapter.nextMethodId + "," + this.className + "#" + name);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (mv == null) {
            return null;
        } else {
            return new MethodAdapter(mv);
        }
    }
}

class MethodAdapter extends MethodVisitor implements Opcodes {
    public MethodAdapter(final MethodVisitor mv) {
        super(ASM7, mv);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("CALL " + name);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        // do call
        mv.visitMethodInsn(opcode, owner, name, desc, itf);
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "err", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("RETURN " + name);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }
}

所以 javaagent 检测适用于小程序。我在 DaCapo benchmark suite 上尝试了 运行 它,它会抛出一个 WhosebugError,如下所示:

Exception: java.lang.WhosebugError thrown from the UncaughtExceptionHandler in thread "main"

当我删除在visitMethodInsn中添加的指令时,代理成功运行。我对此进行了更多研究,并在 ASM docs about having to call MethodVisitor.visitMaxs 中找到了一些东西。这似乎是 WhosebugError 最可能的原因。

所以进一步的问题:

当您注册一个 ClassFileTransformer 时,它将被调用 每个 随后加载 class。这可能包括您正在注入的打印操作本身使用的 classes,如果这些 classes 以前没有使用过的话。您正在为每个方法调用注入打印语句,包括构造函数调用,并且 System.err.println(…) 后面的操作将涉及方法调用和对象构造,因此如果这些被检测到,它们将进入另一个打印操作并且此递归将导致WhosebugError.

显然,安装了 UncaughtExceptionHandler,它试图打印 WhosebugError,它本身以相同的方式进行检测,将再次导致 WhosebugError,因此错误消息读起来像“WhosebugErrorUncaughtExceptionHandler 抛出”。

您应该限制您正在检测的 classes。例如。当 loadernull 时,您不能转换 class 以排除由 bootstrap class 加载程序加载的所有 classes。或者您检查 name 参数以排除以 java. 开头的 classes。或者更详细的解决方案是增强您正在注入的代码,以检测它何时在注入的打印操作中而不是进入递归。

顺便说一下,使用 new ClassReader(klassFileBuffer) 并且不需要 try … catch 块。此外,当您插入像您的代码一样简单的代码时,您可以使用 ClassWriter.COMPUTE_MAXS 而不是 ClassWriter.COMPUTE_FRAMES,以避免对堆栈映射帧进行昂贵的重新计算。因为你没有指定SKIP_FRAMES到reader,它会把原始帧上报给writer,ASM可以适配位置,所以插入一些简单的指令是没有问题的。只有当您插入或删除分支或引入必须跨分支持久存在的变量时,您才需要调整或重新计算帧。