ASM: VisitLabel генерирует слишком много меток и инструкций nop

В документации ASM говорится, что метка представляет собой базовый блок и является узлом в графе управления. Поэтому я тестирую метод visitLabel на этом простом примере:

public static void main(String[] args) {
    int x = 3, y = 4;
    if (x < y) {
        x++;
    }
}

Для метода visitLabel я использую собственный API: setID(int id), где идентификатор является инкрементным. В этом примере CFG должно иметь 3 узла: один в начале и по одному для каждой ветви оператора if. Поэтому я ожидаю, что setID будет вызываться в 3 местах. Однако он вызывается 5 раз, а nop инструкций много. Может ли кто-нибудь объяснить мне, почему?

Вот инструментированный байт-код для вышеуказанной программы.

  public static void main(java.lang.String[]);
    Code:
       0: iconst_2
       1: invokestatic  #13                 // Method setId:(I)V
       4: iconst_3
       5: istore_1
       6: iconst_3
       7: invokestatic  #13                 // Method setId:(I)V
      10: iconst_4
      11: istore_2
      12: iconst_4
      13: invokestatic  #13                 // Method setId:(I)V
      16: iload_1
      17: iload_2
      18: if_icmpge     28
      21: iconst_5
      22: invokestatic  #13                 // Method setId:(I)V
      25: iinc          1, 1
      28: bipush        6
      30: invokestatic  #13                 // Method setId:(I)V
      33: return
      34: nop
      35: nop
      36: nop
      37: nop
      38: athrow

Чего я не понимаю, так это почему перед каждой инструкцией istore стоит label. Нет ветвления, чтобы сделать его новым узлом в CFG.


person qsp    schedule 09.11.2018    source источник


Ответы (1)


Основная цель Label — указать позицию в последовательности байт-кода. Поскольку это необходимо для целей ветвления, вы можете использовать их для идентификации базовых блоков. Но вы должны знать, что они также используются для сообщения номеров строк, когда LineNumberTable присутствует и для сообщения областей действия локальных переменных, когда LocalVariableTable, а для более новых файлов классов аннотации их типов записаны в RuntimeVisibleTypeAnnotations. Кроме того, метки могут помечать защищенную область обработчика исключений. Для кода, сгенерированного из исходного кода Java, эта защищенная область соответствует блоку try, поэтому это базовый блок, но он не должен сохраняться для другого байт-кода.

Видеть

Поскольку область действия локальных переменных может охватывать последнюю инструкцию return, можно встретить метки после этой последней инструкции, что и происходит в вашем случае. Вы вводите bipush 7, invokestatic #13 после инструкции return, что приводит к недостижимости кода.

По-видимому, вы также используете параметры COMPUTE_FRAMES, чтобы позволить ASM пересчитывать кадры карты стека с нуля, но невозможно вычислить кадры для недостижимого кода из-за неизвестного начального состояния стека. ASM решает эту проблему, заменяя недостижимый код инструкциями nop, за которыми следует один оператор athrow. Для этой последовательности можно указать действительный начальный кадр стека, и это не повлияет на выполнение (поскольку код недоступен).

Как видите, четыре инструкции nop плюс одна инструкция athrow занимают пять байтов, что соответствует размеру замененной последовательности bipush 7, invokestatic #13.

Вы можете избавиться от большинства этих ярлыков, указав ClassReader.SKIP_DEBUG на его acceptметод. Затем вы получите только одну сообщенную метку для вашего примера, цель ветви, связанную с оператором if. Но вы должны обрабатывать visitJumpInsn для определения начала условного кода.

Таким образом, чтобы идентифицировать все базовые блоки, вы должны обработать все инструкции ветвления, т.е. через visitJumpInsn, visitLookupSwitchInsn и visitTableSwitchInsn, а также все конечные инструкции, т.е. athrow и все варианты return. Далее нужно обработать все visitTryCatchBlock вызовы. Если вам нужно определить потенциальные цели инструкций ветвления за один проход, я бы использовал visitFrame вместо меток, поскольку фреймы обязательны для всех целей ветвления для файла класса версии 51 (Java 7) или выше.

Кстати, когда все, что вы вводите, — это последовательности загрузки константы и вызова статического метода (в доступных местах), я бы использовал COMPUTE_MAXS вместо COMPUTE_FRAMES, поскольку дорогостоящий пересчет не требуется, когда общая структура кода делает не изменить.

person Holger    schedule 12.11.2018
comment
спасибо за отличный ответ, теперь я понимаю проблему. Использование SKIP_DEBUG удаляет ненужные метки. Тем не менее, я хочу оснастить каждый базовый блок идентификатором, и как мне инструментировать инструкции возврата и ATHROW. Я попытался переопределить visitInsn и инструментировал перед посещением инструкций возврата, но при этом просто добавляется много инструкций nop, как и раньше, и не добавляются вызовы методов вызова. - person qsp; 12.11.2018
comment
Это звучит как новый вопрос. Затем вы должны включить код, выполняющий преобразование, в новый вопрос. В противном случае, действительно трудно сказать. - person Holger; 13.11.2018