java - ASM 和 Javaagent 字节码检测 : ClassFormatError: StackMapTable format error: bad offset for Uninitialized

标签 java instrumentation java-bytecode-asm javaagents jvm-bytecode

我在做什么

我正在使用 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 spec提及:

The bytecode offset at which a frame applies is calculated by adding offset_delta + 1 to the bytecode offset of the previous frame, unless the previous frame is the initial frame of the method, ...

总结

错误原因

bad offset for Uninitialized中的Uninitialized指的是由NEW指令(原始字节码中的第一条指令)产生的未初始化对象).由于原始堆栈映射框架需要在 NEW 指令之前使用标签(比如 L0)来表示其 locals 中的未初始化对象,并且stack,检测代码中的L0不再代表NEW指令,抛出此类错误。

解决方案

由于检测代码中的 L0 不再代表 NEW 指令,我们需要为该 NEW 指令创建一个新标签,并在两个堆栈映射框架中用新标签替换旧标签 L0。如果指定了COMPUTE_FRAME,这样的堆栈映射帧(重新)计算将自动完成,但这里我们需要手动完成,因为COMPUTE_FRAME 没有被使用以避免其他潜在问题.

最佳答案

作为Rafael Winterhalter said ,标签用于引用 NEW 指令,因此将指令放在标签位置和指令之间会破坏此引用。当指令创建的未初始化实例位于堆栈或局部变量中时,堆栈映射框架需要此类引用。

由于在 NEW 指令之前插入代码时要保留原始位置作为分支目标或行号开始,因此必须为注入(inject)代码和插入代码之间的代码位置创建一个新标签old NEW 指令,然后替换堆栈映射框架使用的标签(并且仅替换堆栈映射框架使用的标签)。

请注意,当使用 COMPUTE_FRAMES 选项时,这不是必需的,该选项将从头开始重新计算帧并且不使用旧标签,但是,不使用此选项实际上是一个合理的策略,因为它很昂贵且容易出错。为了注入(inject)没有分支的简单日志记录语句,我们可以保留原始帧,即使检查 NEW 指令的特殊情况会使它稍微困难一些。

以下方法访问者实现上述策略。对于我的测试,我只是注入(inject)了一个普通的 System.out.println(...); 语句。

class CoverageMethodVisitor extends MethodVisitor {
    private final String slashClassName;
    private final String methodName;
    private final Map<Label,Label> translateForUninitialized = new HashMap<>();
    private Label lastLabel;
    private int newLineNumber = -1;

    protected CoverageMethodVisitor(MethodVisitor mv,String clName,String methodName){
        super(Opcodes.ASM9, mv);
        this.slashClassName = clName;
        this.methodName = methodName;
    }

    @Override
    public void visitLineNumber(int line, Label start) {
        newLineNumber = line;
        super.visitLineNumber(line, start);
    }

    @Override
    public void visitLabel(Label label) {
        lastLabel = label;
        super.visitLabel(label);
    }

    @Override
    public void visitTypeInsn(int opcode, String type) {
        if(instrumentReportCoverageInvocation() && opcode == Opcodes.NEW) {
            if(lastLabel != null) {
                Label newLabel = new Label();
                super.visitLabel(newLabel);
                translateForUninitialized.put(lastLabel, newLabel);
            }
        }
        super.visitTypeInsn(opcode, type);
    }

    @Override
    public void visitFrame(int type,
        int numLocal, Object[] local, int numStack, Object[] stack) {
        switch(type) {
            case Opcodes.F_NEW, Opcodes.F_FULL -> {
                local = replaceLabels(numLocal, local);
                stack = replaceLabels(numStack, stack);
            }
            case Opcodes.F_APPEND -> local = replaceLabels(numLocal, local);
            case Opcodes.F_CHOP, Opcodes.F_SAME -> {}
            case Opcodes.F_SAME1 -> stack = replaceLabels(1, stack);
            default -> throw new AssertionError();
        }
        super.visitFrame(type, numLocal, local, numStack, stack);
    }

    private Object[] replaceLabels(int num, Object[] array) {
        Object[] result = array;
        for(int ix = 0; ix < num; ix++) {
            Label repl = translateForUninitialized.get(result[ix]);
            if(repl == null) continue;
            if(result == array) result = array.clone();
            result[ix] = repl;
        }
        return result;
    }

    private boolean instrumentReportCoverageInvocation() {
        int lineNumber = newLineNumber;
        if(lineNumber < 0) return false;
        newLineNumber = -1;
        super.visitFieldInsn(Opcodes.GETSTATIC,
             "java/lang/System", "out", "Ljava/io/PrintStream;");
        super.visitLdcInsn(slashClassName + "." + methodName + " line " + lineNumber);
        super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
            "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        return true;
    }

    @Override
    public void visitLdcInsn(Object value) {
        instrumentReportCoverageInvocation();
        super.visitLdcInsn(value);
    }

    @Override
    public void visitInsn(int opcode) {
        instrumentReportCoverageInvocation();
        super.visitInsn(opcode);
    }

    @Override
    public void visitIntInsn(int opcode, int operand) {
        instrumentReportCoverageInvocation();
        super.visitIntInsn(opcode, operand);
    }

    @Override
    public void visitVarInsn(int opcode, int varIndex) {
        instrumentReportCoverageInvocation();
        super.visitVarInsn(opcode, varIndex);
    }

    @Override
    public void visitFieldInsn(int opcode,
        String owner, String name, String descriptor) {
        instrumentReportCoverageInvocation();
        super.visitFieldInsn(opcode, owner, name, descriptor);
    }

    @Override
    public void visitMethodInsn(int opcode,
        String owner, String name, String descriptor, boolean isInterface) {
        instrumentReportCoverageInvocation();
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }

    @Override
    public void visitInvokeDynamicInsn(
            String name, String descriptor, Handle bootstrapMethodHandle,
            Object... bootstrapMethodArguments) {
        instrumentReportCoverageInvocation();
        super.visitInvokeDynamicInsn(
            name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
    }

    @Override
    public void visitJumpInsn(int opcode, Label label) {
        instrumentReportCoverageInvocation();
        super.visitJumpInsn(opcode, label);
    }

    @Override
    public void visitIincInsn(int varIndex, int increment) {
        instrumentReportCoverageInvocation();
        super.visitIincInsn(varIndex, increment);
    }

    @Override
    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
        instrumentReportCoverageInvocation();
        super.visitTableSwitchInsn(min, max, dflt, labels);
    }

    @Override
    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        instrumentReportCoverageInvocation();
        super.visitLookupSwitchInsn(dflt, keys, labels);
    }

    @Override
    public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
        instrumentReportCoverageInvocation();
        super.visitMultiANewArrayInsn(descriptor, numDimensions);
    }

    @Override
    public AnnotationVisitor visitInsnAnnotation(
        int typeRef, TypePath tp, String desc, boolean visible) {
        instrumentReportCoverageInvocation();
        return super.visitInsnAnnotation(typeRef, tp, desc, visible);
    }
}

我使用了一个直截了当的类访问者

class CoverageClassVisitor extends ClassVisitor {
    private String className;

    CoverageClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }

    @Override
    public void visit(
        int version, int acc, String name, String sig, String superName, String[] ifs) {

        className = name;
        super.visit(version, acc, name, sig, superName, ifs);
    }
    @Override
    public MethodVisitor visitMethod(int access,
        String name, String descriptor, String signature, String[] exceptions) {
        return new CoverageMethodVisitor(
            super.visitMethod(access, name, descriptor, signature, exceptions),
            className,
            name);
    }
}

……以及下面的测试代码

public class ReportLineNumbers {
    public static void main(String[] arg) throws IOException, IllegalAccessException {
        String className = ReportLineNumbers.class.getName() + "$Example";
//        ToolProvider.findFirst("javap")
//            .ifPresent(p -> p.run(System.out, System.err, "-c", className));
        ClassReader cr = new ClassReader(className);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        CoverageClassVisitor trans = new CoverageClassVisitor(cw);
        cr.accept(trans, 0);
        MethodHandles.lookup().defineClass(cw.toByteArray());
        Example.method();
    }
    static class Example {
        static void method() {
            for(int i = 0; i < 3; i++) {
                System.out.println(
                    i < 2?
                    new String(switch(i) {
                        case 0 -> {
//                            try {
                                yield "0";
//                            } catch(Throwable t) {
//                                yield "e";
//                            }
                        }
                        default -> {
                            for(int j = 1; j < 3; j++) {
                                System.out.println(j);
                            }
                            yield "3";
                        }
                    }):
                    new String(i == 2?
                        "4".toCharArray():
                        "5".toCharArray())
                );
            }
        }
    }
}

Example 类的设计尽可能具有挑战性,涵盖NEW 指令前后的不同分支类型(向前、向后、切换)。事实上,事实证明它是如此具有挑战性,以至于我不得不注释掉一个结构,就像 Eclipse 一样,即使没有转换器,它也会产生无效的字节码。但是当使用 javac 时,您可以启用这个内联的 try … catch … 构造,以查看转换器是否会正确处理它。

关于java - ASM 和 Javaagent 字节码检测 : ClassFormatError: StackMapTable format error: bad offset for Uninitialized,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72877465/

相关文章:

java - JAVA中一个对象如何指向许多其他对象?

java - 使用spring集成dsl逐行读取文件

java - 如何单击另一个列表中列表内的链接

java - ASM 如何通知我有关强制转换和构造函数调用的类型注释

java - JFace ColumnWeigthData 导致父级增长

android - api 24 中不推荐使用 Instrumentation 测试用例

java - 使用 Instrumentation 获取对象大小

c++ - 我是不是使用检测例程 (pin) 得到了错误的 ebp,还是我在这里遗漏了什么?

java - 使用 asm Instrumentation 测量 java 程序运行时间

java - 在运行时加载 ASM 生成的类