Как динамически записать все параметры любого вызванного метода java с помощью агента java и ASM? - PullRequest
3 голосов
/ 09 июля 2020

Я хочу записать эти параметры и провести некоторое сравнение с ранее введенными параметрами. Мне нужно записать параметры для каждого вызванного метода, поэтому длина списка параметров не определена.

Думаю, я могу проанализировать дескриптор метода, чтобы узнать, сколько параметров имеет этот метод. Но проблема в том, как записать произвольное количество значений из стека операндов?

Теперь я могу написать только пример кода. В моем MethodVisitorAdapter классе:

public void visitMethodInsn(int opc, String owner, String name, String desc, boolean isInterface){
    int n = getParameterCount(desc);
    // How to duplicate arbitrary number of values at operand stack?
    // ...
    mv.visitMethodInsn(INVOKESTATIC, MyClass, "recordParameters",
                    /* should be what kind of descriptor (may have arbitrary number of parameters)? */, 
                    false);
    mv.visitMethodInsn(opc, owner, name, desc, isInterface);
}

Ответы [ 3 ]

3 голосов
/ 10 июля 2020

Чтобы дублировать произвольную последовательность значений в стеке операндов, невозможно временно сохранить их в новых локальных переменных. Затем pu sh все эти значения в стеке операндов, вызовите свой метод отчетности, pu sh их снова и вызовите исходный метод.

Простой подход, позволяющий избежать * затрат 1004 *, это проверка каждой инструкции, использующей локальную переменную, и запоминание первого свободного индекса. Для этого требуется, чтобы в исходном коде уже были допустимые фреймы карты стека, чтобы правильно обрабатывать обратные ветки за один проход посещения. Для кода, ориентированного на Java 7 или новее, это в любом случае обязательно.

Поскольку код для сообщения о вызове не содержит ветвей и впоследствии не требует временных переменных, нам даже не нужен дорогостоящий пересчет кадры карты стека, как и в точках слияния ветвей, нужны только исходные переменные. В пересчете требуется только максимальный стек и локальные переменные.

Чтобы вызвать метод отчета, мы можем использовать объект-держатель, как в ответ апангина , но в качестве альтернативы мы можем использовать java.lang.invoke package , который позволяет нам на лету генерировать varargs сборщики с точными дескрипторами методов.

Следующий код получит MethodHandle в PrintStream.printf(String,Object...) с помощью одной инструкции ldc, за которой следует привязка System.out в качестве первого аргумента, за которой следует привязка константы String, подходящей для текущего числа аргументов (и получатель для не - static методы), затем адаптируйте вызов дескриптора .asVarargsCollector(Object[].class) .asType(targetType). targetType - это дескриптор типа метода с дополнительным первым типом параметра для нестатических c вызовов метода. Этот MethodType также загружается с одной инструкцией ldc. Затем дескриптор можно использовать, вызывая invokeExact с теми же аргументами, что и фактический вызов в стеке.

Только для вызовов конструктора объект-получатель опускается, как и мы не разрешено использовать объект до его инициализации.

{?store           n + i } for each argumentᵢ
 ldc              MethodHandle invokeVirtual java/io/PrintStream.printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
 getstatic        java/lang/System.out Ljava/io/PrintStream;
 invokevirtual    java/lang/invoke/MethodHandle.bindTo(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
 ldc              String containing <method name> and as many %s place holders as needed
 invokevirtual    java/lang/invoke/MethodHandle.bindTo(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
 ldc              class [Ljava/lang/Object;
 invokevirtual    java/lang/invoke/MethodHandle.asVarargsCollector(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandle;
 ldc              MethodType («actual argument types»)Ljava/io/PrintStream;
 invokevirtual    java/lang/invoke/MethodHandle.asType(Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
{?load            n + i } for each argumentᵢ
 invokevirtual    java/lang/invoke/MethodHandle.invokeExact(«actual argument types»)Ljava/io/PrintStream;
 pop              // remove the PrintStream return by printf
{?load            n + i } for each argumentᵢ
 invoke...        original method

Код преобразования и пример

public class LogMethodCalls {
    public static void main(String[] args) throws IOException, IllegalAccessException {
        MethodHandles.lookup().defineClass(instrument(LogMethodCalls.class
            .getResourceAsStream("LogMethodCalls$ToInstrument.class")));
        runInstrumented();
    }
    private static void runInstrumented() {
        new ToInstrument().run();
    }
    static class ToInstrument implements Runnable {
        @Override
        public void run() {
            double min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
            for(double i: List.of(4, 2, 9, 6)) {
                min = Math.min(min, i);
                max = Math.max(max, i);
            }
            System.out.printf("min %.0f, max %.0f%n", min, max);
        }
    }
    static byte[] instrument(InputStream is) throws IOException {
        ClassReader cr = new ClassReader(is);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);

        cr.accept(new ClassVisitor(Opcodes.ASM7, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name,
                    String descriptor, String signature, String[] exceptions) {
                return new LogInjector(
                    super.visitMethod(access, name, descriptor, signature, exceptions),
                    access, descriptor);
            }
        }, ClassReader.EXPAND_FRAMES);

        return cw.toByteArray();
    }
    static class LogInjector extends MethodVisitor {
        static final String PS_T = "java/io/PrintStream", PS_S = "L" + PS_T + ";";
        static final String PRINTF_DESC="(Ljava/lang/String;[Ljava/lang/Object;)"+PS_S;
        static final String MH_T="java/lang/invoke/MethodHandle", MH_S="L" + MH_T + ";";

        private int firstUnusedVar;
        public LogInjector(MethodVisitor mv, int acc, String desc) {
            super(Opcodes.ASM7, mv);
            int vars = Type.getArgumentsAndReturnSizes(desc) >> 2;
            if((acc & Opcodes.ACC_STATIC) != 0) vars--;
            firstUnusedVar = vars;
        }
        @Override
        public void visitFrame(int type,
            int numLocal, Object[] local, int numStack, Object[] stack) {
            super.visitFrame(type, numLocal, local, numStack, stack);
            firstUnusedVar = Math.max(firstUnusedVar, numLocal);
        }
        @Override
        public void visitVarInsn(int opcode, int var) {
            super.visitVarInsn(opcode, var);
            if(opcode == Opcodes.LSTORE || opcode == Opcodes.DSTORE) var++;
            if(var >= firstUnusedVar) firstUnusedVar = var + 1;
        }
        @Override
        public void visitMethodInsn(int opcode,
            String owner, String name, String descriptor, boolean isInterface) {
            Type[] arg = Type.getArgumentTypes(descriptor);

            int[] vars = storeArguments(arg, opcode, name, owner);

            String reportDesc = getReportDescriptor(owner, descriptor, arg, vars);

            mv.visitLdcInsn(new Handle(Opcodes.H_INVOKEVIRTUAL,
                PS_T, "printf", PRINTF_DESC, false));
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", PS_S);
            bindTo();
            mv.visitLdcInsn(messageFormat(opcode, owner, name, arg));
            bindTo();
            mv.visitLdcInsn(Type.getObjectType("[Ljava/lang/Object;"));
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
                "asVarargsCollector", "(Ljava/lang/Class;)"+MH_S, false);
            mv.visitLdcInsn(Type.getMethodType(reportDesc));
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
                "asType", "(Ljava/lang/invoke/MethodType;)"+MH_S, false);
            pushArguments(arg, vars);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                MH_T, "invokeExact", reportDesc, false);
            mv.visitInsn(Opcodes.POP);

            pushArguments(arg, vars);
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        }

        String getReportDescriptor(
            String owner, String descriptor, Type[] arg, int[] vars) {
            StringBuilder sb = new StringBuilder(owner.length()+descriptor.length()+2);
            sb.append('(');
            if(arg.length != vars.length) {
                if(owner.charAt(0) == '[') sb.append(owner);
                else sb.append('L').append(owner).append(';');
            }
            sb.append(descriptor, 1, descriptor.lastIndexOf(')')+1);
            return sb.append(PS_S).toString();
        }

        int[] storeArguments(Type[] arg, int opcode, String name, String owner) {
            int nArg = arg.length;
            boolean withThis = opcode != Opcodes.INVOKESTATIC && !name.equals("<init>");
            if(withThis) nArg++;
            int[] vars = new int[nArg];
            int slot = firstUnusedVar;
            for(int varIx = nArg-1, argIx = arg.length-1; argIx >= 0; varIx--,argIx--) {
                Type t = arg[argIx];
                mv.visitVarInsn(t.getOpcode(Opcodes.ISTORE), vars[varIx] = slot);
                slot += t.getSize();
            }
            if(withThis)
                mv.visitVarInsn(Opcodes.ASTORE, vars[0] = slot);
            return vars;
        }
        private void bindTo() {
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
                "bindTo", "(Ljava/lang/Object;)"+MH_S, false);
        }
        private void pushArguments(Type[] arg, int[] vars) {
            int vIx = 0;
            if(arg.length != vars.length)
                mv.visitVarInsn(Opcodes.ALOAD, vars[vIx++]);
            for(Type t: arg)
                mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), vars[vIx++]);
        }
        private String messageFormat(int opcode, String owner, String name, Type[] arg){
            StringBuilder sb = new StringBuilder();
            switch(opcode) {
                case Opcodes.INVOKESPECIAL:
                    if(name.equals("<init>")) {
                        name = Type.getObjectType(owner).getClassName();
                        break;
                    }
                    // else no break
                case Opcodes.INVOKEINTERFACE: // no break
                case Opcodes.INVOKEVIRTUAL:
                    sb.append("[%s].");
                    break;
                case Opcodes.INVOKESTATIC:
                    sb.append(Type.getObjectType(owner).getClassName()).append('.');
                    break;
            }
            sb.append(name);
            if(arg.length == 0) sb.append("()%n");
            else {
                sb.append('(');
                for(int i = arg.length; i > 1; i--) sb.append("%s, ");
                sb.append("%s)%n");
            }
            return sb.toString();
        }
    }
}

В примере вызова используется Java 9 и используется JVM с отложенной загрузкой , поэтому он может (пере) определить класс перед его фактическим использованием. Он может быть заменен реальным сценарием инструментария, поскольку он не имеет отношения к фактическому logi c преобразования. В моей настройке пример печатает

java.lang.Object()
java.lang.Integer.valueOf(4)
java.lang.Integer.valueOf(2)
java.lang.Integer.valueOf(9)
java.lang.Integer.valueOf(6)
java.util.List.of(4, 2, 9, 6)
[[4, 2, 9, 6]].iterator()
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[4].intValue()
java.lang.Math.min(2.147483647E9, 4.0)
java.lang.Math.max(-2.147483648E9, 4.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[2].intValue()
java.lang.Math.min(4.0, 2.0)
java.lang.Math.max(4.0, 2.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[9].intValue()
java.lang.Math.min(2.0, 9.0)
java.lang.Math.max(4.0, 9.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[6].intValue()
java.lang.Math.min(2.0, 6.0)
java.lang.Math.max(9.0, 6.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
java.lang.Double.valueOf(2.0)
java.lang.Double.valueOf(9.0)
[java.io.PrintStream@6e5e91e4].printf(min %.0f, max %.0f%n, [Ljava.lang.Object;@2cdf8d8a)
min 2, max 9

Обратите внимание, что ваш вариант использования может быть проще. Если ваш метод ведения журнала является методом static и не требует PrintStream, вам не нужно его связывать. Если он не возвращает значение, вам также не нужно его выталкивать. Было бы еще проще, если бы он принимал переменные аргументы, включая строку формата или имя метода. Затем мы можем передать строку как обычный аргумент, вместо того, чтобы связывать ее, и поскольку дескриптор метода теперь не изменен, он уже будет сборщиком varargs , когда целевой метод - varargs метод:

static void yourLog(Object... arg) {
    String name = (String) arg[0]; // or format string
    arg = Arrays.copyOfRange(arg, 1, arg.length);
    …
}
@Override
public void visitMethodInsn(int opcode,
    String owner, String name, String descriptor, boolean isInterface) {

    Type[] arg = Type.getArgumentTypes(descriptor);
    int[] vars = storeArguments(arg, opcode, name, owner);
    String reportDesc = getReportDescriptor(owner, descriptor, arg, vars);
    mv.visitLdcInsn(new Handle(Opcodes.H_INVOKESTATIC,
        YOUR_TARGET_TYPE, "yourLog", "([Ljava/lang/Object;)V", false));
    mv.visitLdcInsn(Type.getMethodType(reportDesc));
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
        "asType", "(Ljava/lang/invoke/MethodType;)"+MH_S, false);
    mv.visitLdcInsn(messageFormat(opcode, owner, name, arg));
    pushArguments(arg, vars);
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T, "invokeExact", reportDesc, false);

    pushArguments(arg, vars);
    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}

String getReportDescriptor(String owner, String descriptor, Type[] arg, int[] vars) {
    StringBuilder sb = new StringBuilder(owner.length()+descriptor.length()+2);
    sb.append("(Ljava/lang/String;");
    if(arg.length != vars.length) {
        if(owner.charAt(0) == '[') sb.append(owner);
        else sb.append('L').append(owner).append(';');
    }
    sb.append(descriptor, 1, descriptor.lastIndexOf(')')+1);
    return sb.append('V').toString();
}
3 голосов
/ 10 июля 2020

Другой вариант - заменить инструкцию invoke* на invokedynamic и предоставить метод bootstrap:

private static final Handle BSM = new Handle(H_INVOKESTATIC, "com/example/Bootstraps", "invokeProxy", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;)Ljava/lang/invoke/CallSite;", false);

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
    if (opcode == INVOKESPECIAL && name.equals("<init>") {
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        return;
    }
    Type method = Type.getMethodType(descriptor);
    Type[] oldTypes = method.getArgumentTypes();
    final int htype;
    switch (opcode) {
        case INVOKEINTERFACE:
            htype = H_INVOKEINTERFACE;
            break;
        case INVOKESPECIAL:
            htype = H_INVOKESPECIAL;
            break;
        case INVOKESTATIC:
            htype = H_INVOKESTATIC;
            break;
        case INVOKEVIRTUAL:
            htype = H_INVOKEVIRTUAL;
            break;
        default:
            throw new IllegalArgumentException("Unknown opcode: " + opcode);
    }
    final Type[] newTypes;
    switch (opcode) {
        case INVOKESPECIAL:
        case INVOKEINTERFACE:
        case INVOKEVIRTUAL:
            newTypes = new Type[oldTypes.length + 1];
            newTypes[0] = Type.getObjectType(owner);
            System.arraycopy(oldTypes, 0, newTypes, 1, oldTypes.length);
            break;
        case INVOKESTATIC:
            newTypes = oldTypes;
            break;
        default:
            throw new AssertionError(); // can not happen.
    }
    Handle h = new Handle(htype, owner, name, descriptor, isInterface);
    String indyDesc = Type.getMethodType(method.getReturnType(), newTypes).getDescriptor();
    visitInvokeDynamicInsn("_", indyDesc, BSM, h);
}

И простой bootstrap метод вроде этого:

package com.example; // Change the com/example/Bootstraps if you use a different package.

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.util.Arrays;

import static java.lang.invoke.MethodType.methodType;

public class Bootstraps {

    private static final MethodHandle PRINT_ARRAY;

    static {
        try {
            Lookup l = MethodHandles.lookup();
            MethodHandle printLn = l.findVirtual(PrintStream.class, "println", methodType(void.class, String.class))
                    .bindTo(System.err);
            MethodHandle arraysDeepToString = l.findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
            PRINT_ARRAY = MethodHandles.foldArguments(
                    MethodHandles.dropArguments(printLn, 1, Object[].class), arraysDeepToString);
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

    public static CallSite invokeProxy(Lookup lookup, String name, MethodType type, MethodHandle target) {
        Object method = lookup.revealDirect(target);
        MethodHandle printArgsMH = MethodHandles.foldArguments(target, 
                PRINT_ARRAY.asCollector(Object[].class, type.parameterCount() + 1)
                    .bindTo(method)
                    .asType(type.changeReturnType(void.class)))
                .asType(type);
        return new ConstantCallSite(printArgsMH);
    }
}

Что, вероятно, выглядит сложнее, чем есть на самом деле.

3 голосов
/ 10 июля 2020

Вы можете сделать recordParameters методом переменной арности (varargs). Он примет все параметры как один Object[] аргумент.

Итак, вам нужно создать массив и заполнить его (возможно, заключенными в рамку) аргументами. Следующий класс в стиле построителя поможет собрать аргументы из стека. Он содержит push методов для всех возможных типов, а также обрабатывает автобоксинг.

После записи параметров помощник может поместить аргументы обратно в стек, вызвав соответствующие методы pop в обратном порядке.

public class ArgCollector {
    private final Object[] args;
    private int index;

    public ArgCollector(int length) {
        this.args = new Object[length];
        this.index = length;
    }

    public ArgCollector push(Object o) {
        args[--index] = o;
        return this;
    }

    public Object pop() {
        return args[index++];
    }

    public static ArgCollector push(boolean a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(byte    a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(char    a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(short   a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(int     a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(long    a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(float   a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(double  a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(Object  a, ArgCollector c) { return c.push(a); }

    public boolean popZ() { return (boolean) pop(); }
    public byte    popB() { return (byte)    pop(); }
    public char    popC() { return (char)    pop(); }
    public short   popS() { return (short)   pop(); }
    public int     popI() { return (int)     pop(); }
    public long    popJ() { return (long)    pop(); }
    public float   popF() { return (float)   pop(); }
    public double  popD() { return (double)  pop(); }

    public Object[] toArray() {
        return args;
    }
}

Теперь задача состоит в том, чтобы сгенерировать эквивалент байт-кода для следующего Java кода:

    ArgCollector collector = new ArgCollector(N);
    recordParameters(
            ArgCollector.push(arg1,
                ArgCollector.push(arg2,
                    ArgCollector.push(argN, collector)))
            .toArray()
    );

    originalMethod(
            collector.popI(),
            collector.popJ(),
            (String) collector.pop()
    );

Вот как это сделать с помощью ASM:

    Type[] args = Type.getArgumentTypes(desc);
    String collector = Type.getInternalName(ArgCollector.class);

    // new ArgCollector(argCount)
    mv.visitTypeInsn(NEW, collector);
    mv.visitInsn(DUP);
    mv.visitIntInsn(SIPUSH, args.length);
    mv.visitMethodInsn(INVOKESPECIAL, collector, "<init>", "(I)V", false);

    // For each argument call ArgCollector.push(arg, collector)
    for (int i = args.length; --i >= 0; ) {
        Type arg = args[i];
        String argDesc = arg.getDescriptor().length() == 1 ? arg.getDescriptor() : "Ljava/lang/Object;";
        mv.visitMethodInsn(INVOKESTATIC, collector, "push",
                "(" + argDesc + "L" + collector + ";)L" + collector + ";", false);
    }

    // Call recordParameters(collector.toArray())
    mv.visitInsn(DUP);
    mv.visitMethodInsn(INVOKEVIRTUAL, collector, "toArray", "()[Ljava/lang/Object;", false);
    mv.visitMethodInsn(INVOKESTATIC, MyClass, "recordParameters", "([Ljava/lang/Object;)V", false);

    // Push original arguments back on stack
    for (Type arg : args) {
        String argDesc = arg.getDescriptor().length() == 1 ? arg.getDescriptor() : "Ljava/lang/Object;";

        mv.visitInsn(DUP);
        if (argDesc.length() == 1) {
            mv.visitMethodInsn(INVOKEVIRTUAL, collector, "pop" + argDesc, "()" + argDesc, false);
        } else {
            mv.visitMethodInsn(INVOKEVIRTUAL, collector, "pop", "()Ljava/lang/Object;", false);
            if (!arg.getDescriptor().equals("Ljava/lang/Object;")) {
                // Need to cast object arguments to the original type
                mv.visitTypeInsn(CHECKCAST, arg.getDescriptor());
            }
        }

        // Swap the last argument with ArgCollector, so that ArgCollector is on top again
        if (arg.getSize() == 1) {
            mv.visitInsn(SWAP);
        } else {
            mv.visitInsn(DUP2_X1);
            mv.visitInsn(POP2);
        }
    }

    // Pop off the remaining ArgCollector, and call the original method
    mv.visitInsn(POP);
    mv.visitMethodInsn(opc, owner, name, desc, isInterface);
...