«VerifyError: Ожидаете найти объект / массив в стеке» при использовании ASM для отслеживания создания объекта в Java? - PullRequest
3 голосов
/ 20 июня 2020

Я хочу следить за созданием объекта и записывать уникальный идентификатор для этого объекта. Поэтому я использую ASM для отслеживания инструкции «NEW». В моем методе адаптер vistor:

public void visitTypeInsn(int opcode, String desc){
    mv.visitTypeInsn(opcode, desc);
    if (opcode == NEW){
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESTATIC, "org/.../.../MyRecorder", "object_new",
                "(Ljava/lang/Object;)V", false);
    }
}

In MyRecorder.java:

public static void object_new(Object ref){
    log("object_new !");
    log("MyRecorder: " + ref);
    log("ref.getClass().getName(): " + ref.getClass().getName());
}

Однако этот код приводит к java.lang.VerifyError: (...) Expecting to find object/array on stack. Понятия не имею, почему это не может работать. Если этот способ неверен, как я могу отслеживать создание объекта?


На самом деле, я также пытался отслеживать object.<init> вместо того, чтобы отслеживать инструкцию NEW. Однако со следующим кодом он выдает java.lang.VerifyError: (...) Unable to pop operand off an empty stack:

public void visitMethodInsn(int opc, String owner, String name, String desc, boolean isInterface) {
    ...
    mv.visitMethodInsn(opc, owner, name, desc, isInterface);
    if (opc == INVOKESPECIAL && name.equals("<init>")) {
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESTATIC, "org/myekstazi/agent/PurityRecorder", "object_new",
                "(Ljava/lang/Object;)V", false);
    }
}

1 Ответ

0 голосов
/ 23 июня 2020

Создание экземпляра объекта разделено на две инструкции. Это может затруднить работу с экземпляром объекта. На самом деле это была самая большая проблема во многих моих проектах. Простое выражение new Integer(0) может быть скомпилировано в следующий код.

new java/lang/Integer                       ═╤═
dup                                         ═╪═══╤═
iconst_0                                     │   │  ═╤═
invokespecial java/lang/Integer.<init>(I)V   │  ═╧═══╧═
                                             ↓

Ссылка, созданная с помощью new, не может использоваться, пока она не инициализирована с помощью invokespecial. Следовательно, вы не можете ставить инструменты сразу после new. В приведенном выше примере вы можете разместить приборы за invokespecial. Однако вам нужно отличать guish от вызовов суперконструктора. Такой оператор, как super(0), может компилироваться в следующий код.

aload_0                                     ═╤═
iconst_0                                     │  ═╤═
invokespecial {super class}.<init>(I)V      ═╧═══╧═

Эти два примера могут охватывать два наиболее распространенных случая, когда вызывается <init>. Если вы ничего не сделаете со значением, созданным new Integer(0), компилятор также может решить пропустить инструкцию dup. Я не уверен, действительно ли они это делают. В любом случае, первый пример wourld стал следующим кодом.

new java/lang/Integer                       ═╤═
iconst_0                                     │  ═╤═
invokespecial java/lang/Integer.<init>(I)V  ═╧═══╧═

Это все еще можно обработать, вставив dup за new и invokestatic за invokespecial.

В любом случае, если вы хотите обрабатывать еще больше случаев exoti c, байт-код также может содержать такие инструкции, как dup_x1 или dup_x2. GeneratorAdapter.box(Type) фактически генерирует такой код. Некоторые «ленивые» манипуляторы байт-кода могут также вызывать инструкции new, в которых экземпляр никогда не инициализируется до того, как он будет удален с помощью pop. Как видите, работа с экземплярами объектов в байт-коде может занять много работы. Однако, в зависимости от вашей ситуации, вам может не потребоваться поддержка всех этих случаев. К сожалению, у меня нет опыта, чтобы оценить, какие дела вам нужно поддержать.

Вот пример, который не обрабатывает dup_x1 и подобные инструкции. Следовательно, он не будет работать с кодом, созданным GeneratorAdapter.box(Type). Кроме того, в моих тестах он работал неплохо. Для получения информации о стеке операндов используется AnalyzerAdapter.

public final class InstrumentationMethodVisitor extends AnalyzerAdapter {
  public InstrumentationMethodVisitor(
      String owner, int access, String name, String descriptor,
      MethodVisitor methodVisitor) {
    super(ASM8, owner, access, name, descriptor, methodVisitor);
  }

  @Override
  public void visitTypeInsn(int opcode, String type) {
    super.visitTypeInsn(opcode, type);
    if (opcode == NEW && stack != null) {
      mv.visitInsn(DUP);
    }
  }

  @Override
  public void visitInsn(int opcode) {
    if (opcode == POP && stack != null && stack.get(stack.size() - 1) instanceof Label) {
      mv.visitInsn(POP);
    }
    super.visitInsn(opcode);
  }

  @Override
  public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
    if (opcode == INVOKESPECIAL && name.equals("<init>")) {
      boolean hasRelatedNew = stack != null && stack.get(stack.size() - getAmountOfArgumentSlots(descriptor) - 1) instanceof Label;

      super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);

      if (hasRelatedNew) {
        mv.visitMethodInsn(
            INVOKESTATIC,
            "org/myekstazi/agent/PurityRecorder",
            "object_new",
            "(Ljava/lang/Object;)V",
            false);
      }
    }
    else {
      super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }
  }

  private static int getAmountOfArgumentSlots(String descriptor) {
    int slots = 0;
    for (Type type : Type.getArgumentTypes(descriptor)) {
      slots += type.getSize();
    }
    return slots;
  }
}
...