Выбор и изменение оператора if с помощью ASM

Я хочу обновить оператор if в уже существующем классе в определенной строке, не меняя весь метод.

Вот целевой код (имена классов, методов и некоторый код изменены, потому что они неактуальны):

public class Target extends Something {
   public Target(){
        super();
        //some code...
   }

    public Result targetMethod(Data firstPar, Data secondPar){
        if(someStatement()) {
            return Result.FAIL;
        } else {
            if(firstPar.check()){ //here is the line I want to change
                 firstPar.doSomething()
            }
            secondPar.doSomething();
            return Result.SUCCESS;
        }

    }
}

В этом коде я хочу изменить if(firstPar.check()) на что-то вроде этого:

if(firstPar.check() && !Utilities.someOtherCheck(firstPar, secondPar))

Вот как я пытался это решить:

ClassNode node = new ClassNode();
ClassReader reader = new ClassReader(bytes); //bytes - original target class
reader.accept(node, 0);
ClassWriter cw = new ClassWriter(0);
node.accept(cw);
MethodVisitor visitor = cw.visitMethod(ACC_PUBLIC, "targetMethod", "<it's signature>", null, null);
visitor.visitCode();
//No idea what to do next

Вещи, которые я не понимаю:

  1. Как правильно выбрать строку в MethodVisitor?

  2. Как изменить только половину оператора if?

  3. Как мне получить байт-код нового класса (который будет сгенерирован после того, как мы изменим целевой класс)?

Было бы неплохо, если бы вы могли предоставить какой-то пример кода.

Спасибо!


person Daniil Dubrovsky    schedule 06.05.2016    source источник


Ответы (1)


Вопрос 1, пожалуй, самый сложный. Вам нужно будет найти позицию для вставки инструкций, распознав какой-то шаблон. Если вы предполагаете, что firstPar.check() вызывается только один раз, вы можете найти следующие инструкции байт-кода для if(firstPar.check()):

ALOAD 1
INVOKEVIRTUAL Data.check ()Z
IFEQ L3

где L3 — метка перехода, если check возвращает false.

Для вопроса 2 обратите внимание, что инструкции по байт-коду для if(firstPar.check() && !Utilities.someOtherCheck(firstPar, secondPar)):

ALOAD 1
INVOKEVIRTUAL Data.check ()Z
IFEQ L3
ALOAD 1
ALOAD 2
INVOKESTATIC Utilities.someOtherCheck (LData;LData;)Z
IFNE L3

Итак, вам нужно будет вставить 4 новые инструкции сразу после IFEQ L3.

Это можно сделать с помощью Tree API., где вы создаете адаптер для targetMethod путем создания подклассов ClassVisitor и MethodNode:

private static class ClassAdapter extends ClassVisitor {
    public ClassAdapter(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
            String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (name.equals("targetMethod"))
            return new MethodAdapter(access, name, desc, signature, exceptions, mv);
        else
            return mv;
    }
}

private static class MethodAdapter extends MethodNode {
    public MethodAdapter(int access, String name, String desc,
            String signature, String[] exceptions, MethodVisitor mv) {
        super(Opcodes.ASM5, access, name, desc, signature, exceptions);
        this.mv = mv;
    }
    // More to come ...
}

Внутри MethodAdapter вы можете переопределить visitEnd, чтобы перебрать все instructions внутри метода, и попытаться обнаружить 3 вышеуказанные инструкции и вставить 4 новые инструкции после них:

@Override
public void visitEnd() {
    // Iterates all instructions in the method
    ListIterator<AbstractInsnNode> itr = instructions.iterator();
    while (itr.hasNext()) {
        // Checks whether the instruction is ALOAD 1
        AbstractInsnNode node = itr.next();
        if (node.getOpcode() != Opcodes.ALOAD
                || ((VarInsnNode) node).var != 1)
            continue;

        // Checks whether the next instruction is INVOKEVIRTUAL
        if (node.getNext() == null
                || node.getNext().getOpcode() != Opcodes.INVOKEVIRTUAL)
            continue;

        // Checks the invoked method name and signature
        MethodInsnNode next = (MethodInsnNode) node.getNext();
        if (!next.owner.equals("Data")
                || !next.name.equals("check")
                || !next.desc.equals("()Z"))
            continue;

        // Checks whether the next of the next instruction is IFEQ
        AbstractInsnNode next2 = next.getNext();
        if (next2 == null
                || next2.getOpcode() != Opcodes.IFEQ)
            continue;

        // Creates a list instructions to be inserted
        InsnList list = new InsnList();
        list.add(new VarInsnNode(Opcodes.ALOAD, 1));
        list.add(new VarInsnNode(Opcodes.ALOAD, 2));
        list.add(new MethodInsnNode(Opcodes.INVOKESTATIC, 
            "Utilities", "someOtherCheck", 
            "(LData;LData;)Z", false));
        list.add(new JumpInsnNode(Opcodes.IFNE, ((JumpInsnNode) next2).label));

        // Inserts the list, updates maxStack to at least 2, and we are done
        instructions.insert(next2, list);
        maxStack = Math.max(2, maxStack);
        break;
    }
    accept(mv);
}

Чтобы использовать адаптер, вы можете связать его с ClassReader и ClassWriter. Ниже я также цепляю TraceClassVisitor, чтобы распечатать файл журнала в каталоге tmp:

ClassReader reader = new ClassReader("Target");
ClassWriter writer = new ClassWriter(reader, 0);
TraceClassVisitor printer = new TraceClassVisitor(writer, 
        new PrintWriter(System.getProperty("java.io.tmpdir") + File.separator + "Target.log"));
ClassAdapter adapter = new ClassAdapter(printer);
reader.accept(adapter, 0);
byte[] b = writer.toByteArray(); // The modified bytecode
person dejvuth    schedule 06.05.2016
comment
Спасибо за ваш ответ, работает как шарм! Кстати, что означает этот ноль в конструкторе ClassWriter и методе reader#accept? Нет флага со значением 0. Это просто поведение по умолчанию? - person Daniil Dubrovsky; 06.05.2016
comment
@RedEnergy Это варианты при записи и чтении. Вы правы, 0 — это поведение по умолчанию. На самом деле вы только что указали на ошибку. :-) Я должен был также обновить maxStack как минимум до 2, чтобы убедиться, что стек достаточно велик для аргументов someOtherCheck. Смотрите редактирование: я добавил строку. - person dejvuth; 06.05.2016