ASM Javaagent插桩报StackMapTable Uninitialized偏移错误
我基于ASM框架结合Javaagent技术实现类字节码插桩,用于统计代码覆盖率(未使用JaCoCo的原因与本问题无关),核心实现逻辑为:当visitLineNumber方法被回调时,在后续待访问的第一条指令前插入方法调用字节码,记录当前命中的行号。
按照上述逻辑实现插桩后,目标类加载时抛出ClassFormatError,异常栈如下:
java.lang.ClassFormatError: StackMapTable format error: bad offset for Uninitialized in method org.apache.commons.math.ode.ContinuousOutputModelTest.buildInterpolator(D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator; at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) ...
插桩前的方法字节码如下,其中栈映射帧分别位于偏移16(offset_delta=16)、偏移17(offset_delta=0)位置:
private org.apache.commons.math.ode.sampling.StepInterpolator buildInterpolator(double, double[], double); descriptor: (D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator; flags: ACC_PRIVATE Code: stack=7, locals=7, args_size=4 0: new #66 // class org/apache/commons/math/ode/sampling/DummyStepInterpolator 3: dup 4: aload_3 5: dload 4 7: dload_1 8: dcmpl 9: iflt 16 12: iconst_1 13: goto 17 16: iconst_0 17: invokespecial #67 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator."<init>":([DZ)V 20: astore 6 22: aload 6 24: dload_1 25: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V 28: aload 6 30: invokevirtual #69 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.shift:()V 33: aload 6 35: dload 4 37: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V 40: aload 6 42: areturn ... StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 16 locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ] stack = [ uninitialized 0, uninitialized 0, class "[D" ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ] stack = [ uninitialized 0, uninitialized 0, class "[D", int ]
插桩后的方法字节码如下:
private org.apache.commons.math.ode.sampling.StepInterpolator buildInterpolator(double, double[], double); descriptor: (D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator; flags: ACC_PRIVATE Code: stack=10, locals=7, args_size=4 0: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest 3: ldc_w #344 // String buildInterpolator 6: ldc_w #345 // int 169 9: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V 12: new #66 // class org/apache/commons/math/ode/sampling/DummyStepInterpolator 15: dup 16: aload_3 17: dload 4 19: dload_1 20: dcmpl 21: iflt 28 24: iconst_1 25: goto 29 28: iconst_0 29: invokespecial #67 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator."<init>":([DZ)V 32: astore 6 34: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest 37: ldc_w #344 // String buildInterpolator 40: ldc_w #346 // int 170 43: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V 46: aload 6 48: dload_1 49: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V 52: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest 55: ldc_w #344 // String buildInterpolator 58: ldc_w #347 // int 171 61: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V 64: aload 6 66: invokevirtual #69 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.shift:()V 69: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest 72: ldc_w #344 // String buildInterpolator 75: ldc_w #348 // int 172 78: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V 81: aload 6 83: dload 4 85: invokevirtual #68 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V 88: ldc_w #264 // String org/apache/commons/math/ode/ContinuousOutputModelTest 91: ldc_w #344 // String buildInterpolator 94: ldc_w #349 // int 173 97: invokestatic #272 // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V 100: aload 6 102: areturn ... StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 28 locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ] stack = [ uninitialized 0, uninitialized 0, class "[D" ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ] stack = [ uninitialized 0, uninitialized 0, class "[D", int ]
我初步检查未发现StackMapTable存在明显问题,请问该StackMapTable格式非法的根因是什么?
我的插桩实现代码如下:
class CoverageMethodVisitor extends MethodVisitor { private String slashClassName; private String methodName; private int currentLine; private boolean isJUnit3TestClass; private boolean hasTestAnnotation; private boolean isTestMethod; private int classVersion; private boolean isRightAfterLabel; protected CoverageMethodVisitor(MethodVisitor methodVisitor, String className, String methodName, boolean isJUnit3TestClass, int classVersion) { super(ASM_VERSION, methodVisitor); this.slashClassName = className; this.methodName = methodName; this.isJUnit3TestClass = isJUnit3TestClass; this.classVersion = classVersion; } private void instrumentReportCoverageInvocation() { super.visitLdcInsn(slashClassName); super.visitLdcInsn(methodName); super.visitLdcInsn(currentLine); super.visitMethodInsn(INVOKESTATIC, "org/test/cov/CoverageCollector", "reportCoverage", "(Ljava/lang/String;Ljava/lang/String;I)V", false); } @Override public void visitInsn(int opcode) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitInsn(opcode); } @Override public void visitIntInsn(int opcode, int operand) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitIntInsn(opcode, operand); } @Override public void visitVarInsn(int opcode, int varIndex) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitVarInsn(opcode, varIndex); } @Override public void visitTypeInsn(int opcode, String type) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitTypeInsn(opcode, type); } @Override public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitFieldInsn(opcode, owner, name, descriptor); } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } @Override public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); } @Override public void visitJumpInsn(int opcode, Label label) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitJumpInsn(opcode, label); } @Override public void visitLdcInsn(Object value) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitLdcInsn(value); } @Override public void visitIincInsn(int varIndex, int increment) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitIincInsn(varIndex, increment); } @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitTableSwitchInsn(min, max, dflt, labels); } @Override public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitLookupSwitchInsn(dflt, keys, labels); } @Override public void visitMultiANewArrayInsn(String descriptor, int numDimensions) { if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false; super.visitMultiANewArrayInsn(descriptor, numDimensions); } @Override public void visitMaxs(int maxStack, int maxLocals) { super.visitMaxs(maxStack+3, maxLocals); } /** * Should not report line coverage immediately after the visitLineNumber. visitLineNumber is called right after * visitLabel, but it is very possible that a stack map frame is after the label, if insert instructions right * after the label, the original stack map frame will be messed up. So instead, insert instructions before the * first instruction after the label. */ @Override public void visitLineNumber(int line, Label start) { super.visitLineNumber(line, start); currentLine = line; isRightAfterLabel = true; } }
上述代码中/** */的注释是我之前的猜测,不确定是否正确。
补充说明:
我此前误解了offset_delta = 0的含义,根据Java虚拟机规范说明:
栈映射帧对应的字节码偏移计算规则为:除非是方法的初始帧,否则当前帧适用的字节码偏移为前一帧的字节码偏移加上offset_delta再加1。
错误原因初步判断
报错信息中的Uninitialized指向原字节码开头NEW指令生成的未初始化对象。原StackMapTable需要使用NEW指令前的标签(记为L0)来标识局部变量表和操作数栈中的未初始化对象,而插桩后L0对应的位置不再是NEW指令,因此抛出该错误。
可行解决思路
由于插桩后L0不再对应NEW指令,需要为该NEW指令创建新的标签,将两个栈映射帧中旧的L0标签替换为新标签。如果指定COMPUTE_FRAME参数,ASM会自动完成栈映射帧的重计算,但为了避免其他潜在问题我未开启该参数,因此需要手动修正栈映射帧。
内容的提问来源于stack exchange,提问作者Instein

