Как объясняется в этом ответе, подход ASM к вычислению (наиболее конкретного) общего суперкласса не обязательно воспроизводит кадры карты стека оригинальный класс. Он не только должен получить доступ к классам (которые вы могли бы обойти), но может получить доступ к классам, на которые исходный код никогда не ссылался, либо потому, что исходный код использовал более абстрактный тип или тип интерфейса, либо потому, что исходные кадры на самом деле удалил впоследствии неиспользуемое значение вместо объявления объединенного типа.
Таким образом, предпочтительным подходом является вычисление кадров карты стека на основе исходных кадров в соответствии с внесенными вами изменениями кода. Для вашего предполагаемого варианта использования это довольно просто, так как вы не меняете структуру ветвления кода, а просто вводите код, который оставляет состояние стека точно таким, каким оно было до вставленного фрагмента кода.
Так что в принципе можно было бы просто использовать оригинальные кадры. Для этого не указывайте COMPUTE_FRAMES
вместо ClassWriter
и не указывайте SKIP_FRAMES
перед ClassReader
. Вам нужно настроить максимальный размер стека только в том случае, если исходный размер был меньше двух, чтобы обеспечить место для аргументов вашего метода.
Фактические проблемы с вашим агентом возникают из-за попытки использовать строки исходного кода для определения местоположений кода для вставки вызовов. Чтобы проиллюстрировать это, рассмотрим следующий пример:
public class Example {
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
Я использую следующий код, чтобы показать, какие вызовы ASM будут сделаны вашему посетителю:
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("Example");
cr.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(name+desc);
return new PrintingVisitor();
}
}, 0);
}
static class PrintingVisitor extends MethodVisitor {
final Map<Label,Integer> labels = new HashMap<>();
public PrintingVisitor() {
super(Opcodes.ASM5);
}
private String name(Label label) {
return "label_"+labels.merge(label, labels.size(), (a,b) -> a);
}
@Override public void visitCode() {
System.out.println("visitCode()");
}
@Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
System.out.println("visitFrame()");
}
@Override public void visitLabel(Label label) {
System.out.println("."+name(label));
}
@Override public void visitLineNumber(int line, Label start) {
System.out.println(".line "+line+", "+name(start));
}
@Override public void visitJumpInsn(int opcode, Label label) {
System.out.println(get(opcode)+" "+name(label));
}
@Override public void visitInsn(int opcode) {
System.out.println(get(opcode));
}
@Override
public void visitIincInsn(int var, int increment) {
System.out.println("iinc "+var+", "+increment);
}
@Override public void visitEnd() {
System.out.println();
}
}
static String get(int opcode) {
// for simplification, just the ones we need
switch(opcode) {
case Opcodes.RETURN: return "return";
case Opcodes.ICONST_0: return "iconst_0";
case Opcodes.ILOAD: return "iload";
case Opcodes.IF_ICMPGE: return "if_icmpge";
case Opcodes.GOTO: return "goto";
default: return "<"+opcode+">";
}
}
Что производит (при компиляции с javac
):
main([Ljava/lang/String;)V
visitCode()
.label_0
.line 3, label_0
iconst_0
.label_1
visitFrame()
if_icmpge label_2
.label_3
.line 4, label_3
.label_4
.line 3, label_4
iinc 1, 1
goto label_1
.label_2
.line 6, label_2
visitFrame()
return
.label_5
Что демонстрирует:
- «Первая строка», то есть строка 3, сообщается дважды, так как в конце цикла генерируется код, связанный с расположением оператора цикла
for
.
- «Последняя строка», то есть строка 6, сообщается перед
visitFrame()
, которая описывает состояние стека цели перехода конца цикла. label_2
используется как для сообщения строки исходного кода, так и в качестве цели инструкции if_icmpge
. При делегировании вызова visitLabel
ClassWriter
вы определяете цель ветвления, а для цели ветвления требуется фрейм карты стека, поэтому между вызовами visitLabel
и visitFrame
не должно быть кода, но вызов visitLineNumber
, который вы использовали для вставки кода, делается прямо между ними.
Решение:
Внедряйте код прямо в вызов visitCode()
для начала метода. Это происходит до того, как произойдет что-либо еще, и не будет конфликтовать с любой последующей операцией:
@Override public void visitCode() {
super.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "raiseDepth", "()V", false);
}
Для внедрения кода в конце метода просто используйте точные инструкции, которые могут завершить метод, т.е.
@Override public void visitInsn(int opcode) {
switch(opcode) {
case RETURN: case ARETURN: case IRETURN: case LRETURN: case FRETURN: case DRETURN:
case ATHROW:
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "lowerDepth", "()V", false);
}
super.visitInsn(opcode);
}
Обратите внимание, что этого недостаточно, чтобы получить finally
семантику вызова метода в каждом случае. Например. когда вызываемый метод генерирует исключение или среда выполнения генерирует его, например, при разыменовании null
или делении на ноль, метод может не вызываться, но в исходном коде возникла проблема.
Для внедрения кода в произвольные строки исходного кода нет прямого решения. Как показано, строки исходного кода не сопоставляются 1:1 с местоположениями байт-кода, и сообщаемые местоположения могут находиться в местах, где внедрение невозможно. Гораздо лучше выбрать дополнительные критерии, такие как легко идентифицируемая конструкция кода, например, вызов известного метода, чтобы вставить его до или после него.
person
Holger
schedule
25.07.2018
invokestatic
не меняет структуру кода, следовательно, это должно быть возможно при сохранении исходных карт стека. Проблемы могут возникнуть из-за попытки вставки строк исходного кода. Между строками исходного кода и местоположениями байт-кода нет сопоставления 1:1, кроме того, ASM сообщает о строках, используяLabel
s, что может взаимодействовать с тем, как ASM сообщает о кадрах карты стека (то есть также с использованиемLabel
s. - person Holger   schedule 25.07.2018