Ошибка переполнения стека в java.lang.instrument с преобразованием байт-кода ASM - PullRequest
1 голос
/ 18 марта 2019

Я новичок в инструментарии агента Java и инструментарии ASM для байт-кода.Я взял код из этого учебного пособия по UCLA и использовал его для инструментария javagent, используя 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 suitemark , и он выдает ошибку StackOverflowError следующим образом:

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

Когда я удаляю инструкции, добавленные в visitMethodInsn, агент успешно работает.Я исследовал этот бит еще немного и нашел в документах ASM о необходимости вызова MethodVisitor.visitMaxs .Похоже, что это наиболее вероятная причина ошибки StackOverflowError.

Итак, дополнительные вопросы:

  • Это так?Должен ли я позвонить в VisitMaxs в какой-то момент?Если да, то где?
  • Если нет, то что я делаю не так?Или что я должен делать, чтобы избежать переполнения стека?

1 Ответ

2 голосов
/ 18 марта 2019

Когда вы регистрируете ClassFileTransformer, он будет вызываться для каждого впоследствии загруженного класса.Это может включать в себя классы, используемые самой операцией печати, которую вы вводите, если эти классы ранее не использовались.Вы вводите операторы печати для каждого вызова метода, включая вызовы конструктора, и операции, стоящие за System.err.println(…), будут включать вызовы методов и конструкции объектов, поэтому, если они получили инструментарий, они войдут в другую операцию печати, и эта рекурсия приведет к StackOverflowError.

По-видимому, установлен UncaughtExceptionHandler, который пытается напечатать StackOverflowError, который, будучи сам по себе таким же инструментарием, снова приведет к StackOverflowError, поэтому сообщение об ошибке читается как«StackOverflowError выброшено из UncaughtExceptionHandler».

Вы должны ограничить, какие классы вы используете.Например, вы не можете преобразовывать класс, когда loader равен null, чтобы исключить все классы, загруженные загрузчиком классов начальной загрузки.Или вы проверяете аргумент name, чтобы исключить классы, начинающиеся с java..Или более сложным решением было бы улучшить код, который вы вводите, чтобы определить, когда он находится внутри введенной операции печати, и не переходить в рекурсию.

Кстати, используйте new ClassReader(klassFileBuffer) и вам не нужен блок try … catch.Кроме того, когда вы вставляете код, столь же простой, как ваш, вы можете использовать ClassWriter.COMPUTE_MAXS вместо ClassWriter.COMPUTE_FRAMES, чтобы избежать дорогостоящих пересчетов кадров стековой карты.Поскольку вы не указываете SKIP_FRAMES для считывателя, он сообщит об исходных кадрах записывающему устройству, и ASM может адаптировать позиции, поэтому при вставке простых инструкций проблем не возникает.Только когда вы вставляете или удаляете ветки или вводите переменные, которые должны сохраняться в ветвях, вам нужно адаптировать или пересчитать кадры.

...