[英]ASM skips Classes if COMPUTE_FRAMES is set in ClassWriter
我一直在研究与maven-surfire-plugin一起运行的Java代理。 代理应该能够使用ASM库在三个不同的点将方法调用注入到已加载的方法中:1)在每个方法的开头; 2)在每种方法的末尾; 3)在某些行(见下文)。 为此,我实现了一个premain方法,该方法向Java工具中添加了一个新的转换器。 然后,transform方法为应转换的每个类创建一个新的ClassWriter和ClassVisitor(属于ASM库)。
@Override
public void visitLineNumber(int line, Label start) {
if(methodLines.first().equals(line)) {
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "raiseDepth", "()V", false);
}
if(mutationLines != null && mutationLines.contains(line)) {
mv.visitLdcInsn(fqn);
mv.visitLdcInsn(new Integer(line));
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "hitMutation", "(Ljava/lang/String;I)V", false);
}
mv.visitLineNumber(line, start);
if(methodLines.last().equals(line)) {
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "lowerDepth", "()V", false);
}
}
不幸的是,我对此有些麻烦。 如果为ClassWriter
设置了COMPUTE_FRAMES
标志,则不会出现任何错误,但是某些类将被代理跳过并且不进行转换。 经过一番研究,我发现这样做的原因(很可能是)ClassWriter的getCommonSuperClass
方法,该方法预先加载了该类。
如果未设置COMPUTE_FRAMES
标志,则Expected stackmap frame at this location
出现“ Expected stackmap frame at this location
错误,但无法解决。
有人对此问题有解决方案吗?
如此答案中所述 ,ASM计算(最特定)公共超类的方法不一定会重现原始类的堆栈映射框架。 它不仅需要访问类(可以解决),而且可以访问类,而原始代码从未引用过,这是因为原始代码使用了更抽象的类型或接口类型,或者因为原始框架实际上删除随后未使用的值,而不是声明合并的类型。
因此,最好的方法是根据您所做的代码修改,基于原始帧计算堆栈映射帧。 对于您的预期用例,这很简单,因为您无需更改代码的分支结构,而只需注入代码即可使堆栈状态与插入的代码片段之前的状态完全相同。
因此,原则上应该只使用原始帧。 为了实现这一目标,不指定COMPUTE_FRAMES
到ClassWriter
并没有指定SKIP_FRAMES
到ClassReader
。 仅在原始大小小于2的情况下,才需要调整最大堆栈大小,以确保方法参数有足够的空间。
座席的实际问题来自尝试使用源代码行来确定插入呼叫的代码位置。 为了说明这一点,请考虑以下示例:
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
这表明:
for
循环语句的位置关联的代码 visitFrame()
在visitFrame()
之前报告, visitFrame()
描述了循环结束分支目标的堆栈状态。 label_2
既用于报告源代码行,又用作if_icmpge
指令的目标。 在将visitLabel
调用委派给ClassWriter
,您要定义分支目标,而分支目标则需要堆栈映射框架,因此visitLabel
和visitFrame
调用之间必须没有代码,但必须使用用来插入代码的visitLineNumber
调用,就在他们之间。 解决方案:
直接在visitCode()
调用中插入代码以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映射到字节码位置,并且报告的位置可能在无法注入的位置。 最好选择其他准则,例如易于识别的代码构造,例如已知的方法调用,以在其之前或之后插入。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.